# Skipo API > The Skipo public API — reference, guides, and code examples This file contains all documentation content in a single document following the llmstxt.org standard. ## Account {/* AUTO-GENERATED by scripts/gen-api-reference.mjs — do not edit; run `yarn gen:api`. */} # Account ## `GET /v2/account` Get the account for the authenticated key **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | [`AccountResource`](#accountresource) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## Esquemas ### AccountResource | Campo | Tipo | Descripción | |---|---|---| | `id` | `string` | | | `username` | `string` | | | `email` | `string` | | | `fullName` | `string` | Ej.: `Jane Doe`. | | `nationalId` | `string` \| `null` | | | `verificationStatus` | `string` | | | `preferredCurrency` | `string` | | ### Problem | Campo | Tipo | Descripción | |---|---|---| | `type` | `string` | Stable problem-type URI. Resolves to docs for this error. Ej.: `https://docs.skipo.com/errors/rate_limited`. | | `title` | `string` | Short, human-readable summary (stable, English). Ej.: `Rate limit exceeded`. | | `status` | `number` | HTTP status code. Ej.: `429`. | | `code` | `string` | Stable machine-readable error code (equals the last path segment of `type`). Ej.: `rate_limited`. | | `retryable` | `boolean` | Whether retrying the identical request may succeed. Ej.: `true`. | | `detail` *(opcional)* | `string` | Human-readable, possibly localized detail. | | `instance` *(opcional)* | `string` | The request path that produced the error. | | `traceId` *(opcional)* | `string` | Trace id — joins BigQuery api_request and Cloud Logging. | --- Volver a la [referencia de endpoints](/api). Ver también: [Paginación](/concepts/pagination) · [Errores](/concepts/errors) · [Ids y correlación](/concepts/ids-and-correlation). --- ## Assets {/* AUTO-GENERATED by scripts/gen-api-reference.mjs — do not edit; run `yarn gen:api`. */} # Assets ## `GET /v2/assets` List supported assets **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `page` | query | `integer` | no | Default: `1`. | | `limit` | query | `integer` | no | Default: `25`. | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | `data`: [`AssetResource`](#assetresource)[] · `pagination`: [`PaginationMeta`](#paginationmeta) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `GET /v2/assets/{assetSymbol}` Get a supported asset by its symbol Reference data is identical for every key, so this response does not depend on who asks. **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `assetSymbol` | path | `string` | sí | The asset code published as `assetSymbol` by `GET /v2/assets` (e.g. `BTC`, `NVDASPV`). Case-insensitive. | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | [`AssetResource`](#assetresource) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `404` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`not_found`](https://docs.skipo.com/errors/not_found) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## Esquemas ### AssetResource | Campo | Tipo | Descripción | |---|---|---| | `assetSymbol` | `string` | Asset code. Use it wherever an endpoint asks for an `asset`. Ej.: `BTC`. | | `assetName` | `string` | Ej.: `Bitcoin`. | | `assetClass` | `FIAT` \| `STABLECOIN` \| `STOCK` \| `CRYPTO` | What kind of asset this is. `STOCK` is a tokenised equity (xStock) and is the only class whose amounts rebase — see `rebaseMultiplier`. Do not infer this from any numeric field. Ej.: `CRYPTO`. | | `amountIncrement` | `string` | Smallest amount step Skipo operates on for this asset. An amount finer than this is TRUNCATED — not rejected, and not rounded up. Ej.: `0.00000001`. | | `displayIncrement` | `string` | Step Skipo uses when DISPLAYING this asset — coarser than `amountIncrement`. Presentation only; never validate an amount against it. Ej.: `0.01`. | | `minimumDeposit` | `string` | Smallest deposit that will be credited. Ej.: `0.0001`. | | `minimumWithdrawal` | `string` | Smallest withdrawal to an external destination. Ej.: `0.001`. | | `minimumInternalWithdrawal` | `string` | Smallest withdrawal to another Skipo account. Usually lower than the external minimum, because no network fee is involved. Ej.: `0.0001`. | | `withdrawalFee` | `string` | Flat fee the account pays Skipo for an external withdrawal of this asset, charged in that same asset. `"0"` for fiat. Deposits are always free. Ej.: `0.0005`. | | `maxIndicativeQuoteAmount` | `string` | Largest amount Skipo will quote for an account with no balance and no available credit. A quote within this limit is INDICATIVE — priced, but confirming it still requires funds. Ej.: `0.2`. | | `rebaseMultiplier` | `string` | Rebase factor currently in effect for a tokenised equity: one share = `rebaseMultiplier` settled tokens. `"1"` for every non-rebasing asset. This is the CURRENT factor — to render a PAST trade, use the point-in-time `multiplier` on the fill, never this one. Ej.: `1`. | | `networks` | [`NetworkResource`](#networkresource)[] | Networks this asset can be withdrawn over. Empty for fiat. | ### NetworkResource | Campo | Tipo | Descripción | |---|---|---| | `networkSymbol` | `string` | Upstream network identifier, passed through UNNORMALIZED — do not treat it as a stable key or parse it. Formats are inconsistent and mix three unrelated kinds of value: chain symbols (`BTC`, `SOL`, `ETH`), token standards that are not chains at all (`ERC20`, `BEP20`), and prose (`Dogecoin`, `Stellar Network`, `XRP Ledger`). `BEP20` and `BSC` are the same chain under two names. A canonical network registry is owned by maintainers (SPEC Phase 2g); until it lands this field is descriptive only, and a later release will replace it with normalized values. Use `assetSymbol` as the key for anything durable. Ej.: `BTC`. | | `networkName` | `string` | Human-readable network name. Ej.: `Bitcoin`. | | `withdrawalFee` | `string` | Fee charged for a withdrawal over this network, denominated in the asset itself. Ej.: `0.0005`. | ### PaginationMeta | Campo | Tipo | Descripción | |---|---|---| | `page` | `number` | Ej.: `1`. | | `limit` | `number` | Ej.: `25`. | | `totalItems` | `number` | Ej.: `100`. | | `totalPages` | `number` | Ej.: `4`. | ### Problem | Campo | Tipo | Descripción | |---|---|---| | `type` | `string` | Stable problem-type URI. Resolves to docs for this error. Ej.: `https://docs.skipo.com/errors/rate_limited`. | | `title` | `string` | Short, human-readable summary (stable, English). Ej.: `Rate limit exceeded`. | | `status` | `number` | HTTP status code. Ej.: `429`. | | `code` | `string` | Stable machine-readable error code (equals the last path segment of `type`). Ej.: `rate_limited`. | | `retryable` | `boolean` | Whether retrying the identical request may succeed. Ej.: `true`. | | `detail` *(opcional)* | `string` | Human-readable, possibly localized detail. | | `instance` *(opcional)* | `string` | The request path that produced the error. | | `traceId` *(opcional)* | `string` | Trace id — joins BigQuery api_request and Cloud Logging. | --- Volver a la [referencia de endpoints](/api). Ver también: [Paginación](/concepts/pagination) · [Errores](/concepts/errors) · [Ids y correlación](/concepts/ids-and-correlation). --- ## Balances {/* AUTO-GENERATED by scripts/gen-api-reference.mjs — do not edit; run `yarn gen:api`. */} # Balances ## `GET /v2/balances` List the account asset balances **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | `object` | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `GET /v2/balances/{assetSymbol}/history` Historical balances for an asset **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `assetSymbol` | path | `string` | sí | The asset code published as `assetSymbol` by `GET /v2/assets`. | | `page` | query | `integer` | no | Default: `1`. | | `limit` | query | `integer` | no | Default: `25`. | | `startDate` | query | `string` | no | ISO-8601 start date. Defaults to the first movement. | | `endDate` | query | `string` | no | ISO-8601 end date. Defaults to now. | | `granularity` | query | `day` \| `week` \| `month` | no | Default: `day`. | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | [`HistoricalBalancesResponse`](#historicalbalancesresponse) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `404` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`not_found`](https://docs.skipo.com/errors/not_found) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## Esquemas ### BalanceResource | Campo | Tipo | Descripción | |---|---|---| | `assetSymbol` | `string` | Ej.: `BTC`. | | `balance` | `string` | Ej.: `500`. | | `balanceFrozen` | `string` | Ej.: `0`. | | `balancePending` | `string` | Ej.: `100`. | | `balancePrefCurrency` | `string` | This balance converted to the account's preferred currency. The unit is not repeated here — read `preferredCurrency` from `GET /v2/account`. Named for its unit, like `balanceUSD`. Ej.: `500`. | | `balanceUSD` | `string` | Ej.: `0.62`. | | `balanceCapacityTotal` *(opcional)* | `string` | | | `balanceCapacityAvailable` *(opcional)* | `string` | | | `balanceCapacitySpent` *(opcional)* | `string` | | ### HistoricalBalancePoint | Campo | Tipo | Descripción | |---|---|---| | `date` | `string` | | | `balance` | `string` | Balance at the end of the period, as an exact decimal string in plain notation — never exponential. Truncated to the asset operating precision. Parse it as a decimal, not a float. Ej.: `500.62`. | ### HistoricalBalancesResponse | Campo | Tipo | Descripción | |---|---|---| | `assetSymbol` | `string` | Ej.: `BTC`. | | `granularity` | `day` \| `week` \| `month` | | | `startDate` | `string` \| `null` | | | `endDate` | `string` \| `null` | | | `data` | [`HistoricalBalancePoint`](#historicalbalancepoint)[] | | | `pagination` | [`PaginationMeta`](#paginationmeta) | | ### PaginationMeta | Campo | Tipo | Descripción | |---|---|---| | `page` | `number` | Ej.: `1`. | | `limit` | `number` | Ej.: `25`. | | `totalItems` | `number` | Ej.: `100`. | | `totalPages` | `number` | Ej.: `4`. | ### Problem | Campo | Tipo | Descripción | |---|---|---| | `type` | `string` | Stable problem-type URI. Resolves to docs for this error. Ej.: `https://docs.skipo.com/errors/rate_limited`. | | `title` | `string` | Short, human-readable summary (stable, English). Ej.: `Rate limit exceeded`. | | `status` | `number` | HTTP status code. Ej.: `429`. | | `code` | `string` | Stable machine-readable error code (equals the last path segment of `type`). Ej.: `rate_limited`. | | `retryable` | `boolean` | Whether retrying the identical request may succeed. Ej.: `true`. | | `detail` *(opcional)* | `string` | Human-readable, possibly localized detail. | | `instance` *(opcional)* | `string` | The request path that produced the error. | | `traceId` *(opcional)* | `string` | Trace id — joins BigQuery api_request and Cloud Logging. | --- Volver a la [referencia de endpoints](/api). Ver también: [Paginación](/concepts/pagination) · [Errores](/concepts/errors) · [Ids y correlación](/concepts/ids-and-correlation). --- ## Contacts {/* AUTO-GENERATED by scripts/gen-api-reference.mjs — do not edit; run `yarn gen:api`. */} # Contacts ## `GET /v2/contacts` List contacts **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `page` | query | `integer` | no | Default: `1`. | | `limit` | query | `integer` | no | Default: `25`. | | `type` | query | `INTERNAL` \| `EXTERNAL_CRYPTO` \| `BANK_ACCOUNT` | no | Filter by contact type. | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | `data`: [`ContactResource`](#contactresource)[] · `pagination`: [`PaginationMeta`](#paginationmeta) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `GET /v2/contacts/{contactId}` Get a contact by id **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `contactId` | path | `string` | sí | | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | [`ContactResource`](#contactresource) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `404` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`not_found`](https://docs.skipo.com/errors/not_found) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `PATCH /v2/contacts/{contactId}` Edit a contact's reference/alias **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `contactId` | path | `string` | sí | | **Cuerpo de la petición** — [`UpdateContactReferenceDto`](#updatecontactreferencedto) **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | [`ContactResource`](#contactresource) | | `400` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`validation_error`](https://docs.skipo.com/errors/validation_error) (not retryable). | — | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `404` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`not_found`](https://docs.skipo.com/errors/not_found) (not retryable). | — | | `422` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unprocessable`](https://docs.skipo.com/errors/unprocessable) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## Esquemas ### BankContactDetails | Campo | Tipo | Descripción | |---|---|---| | `assetSymbol` | `string` \| `null` | Asset this account settles in. Always a FIAT code — a bank account cannot be denominated in a crypto asset or a tokenised equity. Ej.: `CLP`. | | `nationalId` | `string` \| `null` | The account holder's national id. | | `bankId` | `string` \| `null` | | | `bankName` | `string` \| `null` | Ej.: `Banco de Chile`. | | `accountTypeId` | `string` \| `null` | | | `bankAccountType` | `string` \| `null` | Ej.: `CHECKING`. | | `bankAccountNumber` | `string` \| `null` | | | `accountEmail` | `string` \| `null` | | ### ContactResource | Campo | Tipo | Descripción | |---|---|---| | `id` | `string` | | | `reference` | `string` \| `null` | | | `type` | `INTERNAL` \| `EXTERNAL_CRYPTO` \| `BANK_ACCOUNT` | Which kind of destination this is, and therefore which detail block is present: `EXTERNAL_CRYPTO` → `crypto`, `BANK_ACCOUNT` → `bank`, `INTERNAL` → neither. | | `alias` | `string` | | | `createdAt` | `string` | | | `updatedAt` | `string` | | | `crypto` *(opcional)* | `any` | Present only when `type` is `EXTERNAL_CRYPTO`. | | `bank` *(opcional)* | `any` | Present only when `type` is `BANK_ACCOUNT`. | ### CryptoContactDetails | Campo | Tipo | Descripción | |---|---|---| | `assetSymbol` | `string` \| `null` | Asset code this address receives. Ej.: `BTC`. | | `assetName` | `string` \| `null` | Ej.: `Bitcoin`. | | `networkSymbol` | `string` \| `null` | Network the address lives on. Ej.: `BTC`. | | `networkName` | `string` \| `null` | Ej.: `Bitcoin`. | | `address` | `string` \| `null` | The on-chain destination address. | | `tag` | `string` \| `null` | Destination tag / memo, for chains that require one (XRP, XLM…). Null otherwise. | ### PaginationMeta | Campo | Tipo | Descripción | |---|---|---| | `page` | `number` | Ej.: `1`. | | `limit` | `number` | Ej.: `25`. | | `totalItems` | `number` | Ej.: `100`. | | `totalPages` | `number` | Ej.: `4`. | ### Problem | Campo | Tipo | Descripción | |---|---|---| | `type` | `string` | Stable problem-type URI. Resolves to docs for this error. Ej.: `https://docs.skipo.com/errors/rate_limited`. | | `title` | `string` | Short, human-readable summary (stable, English). Ej.: `Rate limit exceeded`. | | `status` | `number` | HTTP status code. Ej.: `429`. | | `code` | `string` | Stable machine-readable error code (equals the last path segment of `type`). Ej.: `rate_limited`. | | `retryable` | `boolean` | Whether retrying the identical request may succeed. Ej.: `true`. | | `detail` *(opcional)* | `string` | Human-readable, possibly localized detail. | | `instance` *(opcional)* | `string` | The request path that produced the error. | | `traceId` *(opcional)* | `string` | Trace id — joins BigQuery api_request and Cloud Logging. | ### UpdateContactReferenceDto | Campo | Tipo | Descripción | |---|---|---| | `reference` | `string` | New reference/alias label for the contact (does not change the destination). | --- Volver a la [referencia de endpoints](/api). Ver también: [Paginación](/concepts/pagination) · [Errores](/concepts/errors) · [Ids y correlación](/concepts/ids-and-correlation). --- ## Deposits {/* AUTO-GENERATED by scripts/gen-api-reference.mjs — do not edit; run `yarn gen:api`. */} # Deposits ## `GET /v2/deposits` List deposits **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `page` | query | `integer` | no | Default: `1`. | | `limit` | query | `integer` | no | Default: `25`. | | `assetSymbol` | query | `string` | no | | | `status` | query | `string` | no | | | `subType` | query | `INTERNAL` \| `EXTERNAL_CRYPTO` \| `EXTERNAL_FIAT_BANK` \| `EXTERNAL_FIAT_REDPAY_CHARGEBACK` | no | | | `startDate` | query | `string` | no | ISO-8601 start date. | | `endDate` | query | `string` | no | ISO-8601 end date. | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | `data`: [`DepositResource`](#depositresource)[] · `pagination`: [`PaginationMeta`](#paginationmeta) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `GET /v2/deposits/{id}` Get a deposit by id **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `id` | path | `string` | sí | | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | [`DepositResource`](#depositresource) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `404` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`not_found`](https://docs.skipo.com/errors/not_found) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## Esquemas ### DepositResource | Campo | Tipo | Descripción | |---|---|---| | `id` | `string` | Deposit id. Use it to re-fetch this deposit and to match its ledger entry (`source.id`). | | `type` | `string` | Ej.: `DEPOSIT`. | | `subType` | `INTERNAL` \| `EXTERNAL_CRYPTO` \| `EXTERNAL_FIAT_BANK` \| `EXTERNAL_FIAT_REDPAY_CHARGEBACK` | | | `assetSymbol` | `string` | Asset code of the deposit. Ej.: `BTC`. | | `amount` | `string` | Amount deposited. Positive magnitude, in `assetSymbol`. | | `fee` | `string` | Fee charged by Skipo for this deposit. Always zero — Skipo does not charge deposit fees. Present for a consistent shape across resources. Ej.: `0`. | | `total` | `string` | Total credited to the balance. Positive magnitude, in `assetSymbol`. | | `status` | `string` | | | `createdAt` | `string` | | | `depositData` | `object` | Sub-type-specific details. | ### PaginationMeta | Campo | Tipo | Descripción | |---|---|---| | `page` | `number` | Ej.: `1`. | | `limit` | `number` | Ej.: `25`. | | `totalItems` | `number` | Ej.: `100`. | | `totalPages` | `number` | Ej.: `4`. | ### Problem | Campo | Tipo | Descripción | |---|---|---| | `type` | `string` | Stable problem-type URI. Resolves to docs for this error. Ej.: `https://docs.skipo.com/errors/rate_limited`. | | `title` | `string` | Short, human-readable summary (stable, English). Ej.: `Rate limit exceeded`. | | `status` | `number` | HTTP status code. Ej.: `429`. | | `code` | `string` | Stable machine-readable error code (equals the last path segment of `type`). Ej.: `rate_limited`. | | `retryable` | `boolean` | Whether retrying the identical request may succeed. Ej.: `true`. | | `detail` *(opcional)* | `string` | Human-readable, possibly localized detail. | | `instance` *(opcional)* | `string` | The request path that produced the error. | | `traceId` *(opcional)* | `string` | Trace id — joins BigQuery api_request and Cloud Logging. | --- Volver a la [referencia de endpoints](/api). Ver también: [Paginación](/concepts/pagination) · [Errores](/concepts/errors) · [Ids y correlación](/concepts/ids-and-correlation). --- ## Referencia de endpoints {/* AUTO-GENERATED by scripts/gen-api-reference.mjs — do not edit; run `yarn gen:api`. */} # Referencia de endpoints Esta es la referencia **en texto** de la API v2, generada desde la especificación OpenAPI. Para probar las llamadas de forma interactiva usa la [Referencia de la API](/reference). | Sección | Endpoints | |---|---| | [Account](/api/account) | `GET /v2/account` | | [Assets](/api/assets) | `GET /v2/assets` · `GET /v2/assets/{assetSymbol}` | | [Balances](/api/balances) | `GET /v2/balances` · `GET /v2/balances/{assetSymbol}/history` | | [Contacts](/api/contacts) | `GET /v2/contacts` · `GET /v2/contacts/{contactId}` · `PATCH /v2/contacts/{contactId}` | | [Deposits](/api/deposits) | `GET /v2/deposits` · `GET /v2/deposits/{id}` | | [Ledger](/api/ledger) | `GET /v2/ledger` · `GET /v2/ledger/{id}` | | [Markets](/api/markets) | `GET /v2/markets` · `GET /v2/markets/{market}` · `GET /v2/markets/{market}/price` | | [Orders](/api/orders) | `GET /v2/fills` · `GET /v2/orders` · `POST /v2/orders` · `GET /v2/orders/{id}` · `GET /v2/orders/{id}/fills` · `POST /v2/quotes` | | [System](/api/system) | `GET /v2/health` · `GET /v2/time` | | [Webhooks](/api/webhooks) | `GET /v2/.well-known/webhook-jwks.json` | | [Withdrawals](/api/withdrawals) | `GET /v2/withdrawals` · `POST /v2/withdrawals` · `GET /v2/withdrawals/{id}` | Ver también: [Paginación](/concepts/pagination) · [Errores](/concepts/errors) · [Ids y correlación](/concepts/ids-and-correlation). --- ## Ledger {/* AUTO-GENERATED by scripts/gen-api-reference.mjs — do not edit; run `yarn gen:api`. */} # Ledger ## `GET /v2/ledger` List ledger entries The double-entry record of how a balance changed, for one asset. Every entry carries a `source` back-pointer to the withdrawal, deposit or fill that caused it; filter by `sourceId` to fetch exactly those entries, or by `orderId` to fetch every entry an order produced across all of its fills. **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `page` | query | `integer` | no | Default: `1`. | | `limit` | query | `integer` | no | Default: `25`. | | `assetSymbol` | query | `string` | sí | Asset to list entries for. Required. | | `type` | query | `BUY` \| `SELL` \| `DEPOSIT` \| `WITHDRAWAL` | no | Filter by entry type. | | `sourceId` | query | `string` | no | Return only the entries caused by this resource. Accepts a withdrawal, deposit or fill id — the same value the resource publishes as `id` and the entry echoes as `source.id`. | | `orderId` | query | `string` | no | Return every entry produced by an order, across all of its fills. The ledger does not store an order id, so this resolves the order to its fills first — one extra internal hop. Note an order settles in two assets, and `asset` selects which leg you see. | | `startDate` | query | `string` | no | ISO-8601 start date. | | `endDate` | query | `string` | no | ISO-8601 end date. | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | [`LedgerEntriesResponse`](#ledgerentriesresponse) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `GET /v2/ledger/{id}` Get a ledger entry by id **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `id` | path | `string` | sí | | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | [`LedgerEntry`](#ledgerentry) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `404` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`not_found`](https://docs.skipo.com/errors/not_found) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## Esquemas ### LedgerEntriesResponse | Campo | Tipo | Descripción | |---|---|---| | `startDate` | `string` \| `null` | | | `endDate` | `string` \| `null` | | | `data` | [`LedgerEntry`](#ledgerentry)[] | | | `pagination` | [`PaginationMeta`](#paginationmeta) | | ### LedgerEntry | Campo | Tipo | Descripción | |---|---|---| | `id` | `string` | Id of this ledger entry. | | `source` | `any` | The resource that caused this entry. | | `type` | `string` | Ej.: `DEPOSIT`. | | `subType` | `string` \| `null` | What KIND of entry this is within its type. Without it a DEPOSIT booked by an earn distribution is indistinguishable from a customer deposit. Ej.: `INTERNAL_EARN_DISTRIBUTION`. | | `assetSymbol` | `string` | Ej.: `BTC`. | | `detail` | `string` | | | `amount` | `string` | Positive magnitude; direction is given by `type`. Ej.: `500`. | | `fee` | `string` | Positive magnitude. Ej.: `0`. | | `total` | `string` | Positive magnitude. Ej.: `500`. | | `balance` | `string` | Running balance after this entry. Signed — this is a state, not a movement. Ej.: `500.62`. | | `createdAt` | `string` | | ### LedgerEntrySource | Campo | Tipo | Descripción | |---|---|---| | `type` | `withdrawal` \| `deposit` \| `fill` \| `other` | Ej.: `withdrawal`. | | `id` | `string` | Id of the causing resource. Matches that resource’s `id`. | ### PaginationMeta | Campo | Tipo | Descripción | |---|---|---| | `page` | `number` | Ej.: `1`. | | `limit` | `number` | Ej.: `25`. | | `totalItems` | `number` | Ej.: `100`. | | `totalPages` | `number` | Ej.: `4`. | ### Problem | Campo | Tipo | Descripción | |---|---|---| | `type` | `string` | Stable problem-type URI. Resolves to docs for this error. Ej.: `https://docs.skipo.com/errors/rate_limited`. | | `title` | `string` | Short, human-readable summary (stable, English). Ej.: `Rate limit exceeded`. | | `status` | `number` | HTTP status code. Ej.: `429`. | | `code` | `string` | Stable machine-readable error code (equals the last path segment of `type`). Ej.: `rate_limited`. | | `retryable` | `boolean` | Whether retrying the identical request may succeed. Ej.: `true`. | | `detail` *(opcional)* | `string` | Human-readable, possibly localized detail. | | `instance` *(opcional)* | `string` | The request path that produced the error. | | `traceId` *(opcional)* | `string` | Trace id — joins BigQuery api_request and Cloud Logging. | --- Volver a la [referencia de endpoints](/api). Ver también: [Paginación](/concepts/pagination) · [Errores](/concepts/errors) · [Ids y correlación](/concepts/ids-and-correlation). --- ## Markets {/* AUTO-GENERATED by scripts/gen-api-reference.mjs — do not edit; run `yarn gen:api`. */} # Markets ## `GET /v2/markets` List supported markets **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `page` | query | `integer` | no | Default: `1`. | | `limit` | query | `integer` | no | Default: `25`. | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | `data`: [`MarketResource`](#marketresource)[] · `pagination`: [`PaginationMeta`](#paginationmeta) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `GET /v2/markets/{market}` Get a supported market by id The id is the market code returned by `GET /v2/markets` (e.g. `BTC-CLP`). Reference data is identical for every key, so this response does not depend on who asks. **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `market` | path | `string` | sí | | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | [`MarketResource`](#marketresource) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `404` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`not_found`](https://docs.skipo.com/errors/not_found) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `GET /v2/markets/{market}/price` Get an indicative price for a market A NON-BINDING price. Nothing is reserved and there is no expiry — the response carries no order id, so it cannot be executed. To obtain a confirmable quote use `POST /v2/quotes` (scope `trading:write`), then execute it with a signed `POST /v2/orders`. Skipo prices a dealer spread, so the rate depends on size: pass `amount` + `amountAsset` to price a specific ticket, or omit both to price at the market minimum. `amount` is capped by that asset’s `maxIndicativeQuoteAmount` (see `GET /v2/assets`); above the cap the request is rejected rather than silently repriced at a smaller size. **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `market` | path | `string` | sí | | | `side` | query | `BUY` \| `SELL` | sí | Which direction to price. Required — buying and selling the same market are different prices, so there is no sensible default. | | `amount` | query | `string` | no | Amount to price, denominated in `amountAsset`. A decimal string. Requires `amountAsset`. Omit both to price at the market minimum. Capped by the `maxIndicativeQuoteAmount` of `amountAsset` from `GET /v2/assets` — an amount above the cap is rejected rather than silently reduced, because a price for an amount you did not ask for is worse than an error. | | `amountAsset` | query | `string` | no | Which leg `amount` is denominated in — must be one of the market's two assets. Requires `amount`. | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | [`PriceResource`](#priceresource) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `404` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`not_found`](https://docs.skipo.com/errors/not_found) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## Esquemas ### MarketResource | Campo | Tipo | Descripción | |---|---|---| | `id` | `string` | Ej.: `BTC-CLP`. | | `baseAsset` | `string` | Base asset code. Matches a `GET /v2/assets` assetSymbol. Ej.: `BTC`. | | `quoteAsset` | `string` | Quote asset code. Matches a `GET /v2/assets` assetSymbol. Ej.: `CLP`. | | `type` | `CRYPTO-CRYPTO` \| `CRYPTO-FIAT` \| `FIAT-FIAT` | The asset classes this market pairs. It does NOT identify a tokenised-equity market — those are typed `CRYPTO-FIAT` too. Read `assetClass` on the asset for that. Ej.: `CRYPTO-FIAT`. | | `minBaseAmount` | `string` | Smallest amount you may request when you denominate the trade in the BASE asset. Identical for BUY and SELL. Ej.: `0.0001`. | | `minQuoteAmount` | `string` | Smallest amount you may request when you denominate the trade in the QUOTE asset. Identical for BUY and SELL. Ej.: `1000`. | | `baseIncrement` | `string` | Amount step on the BASE leg. A finer amount is truncated, not rejected. Equivalent to Coinbase's `base_increment` and Binance's `stepSize`. Ej.: `0.00000001`. | | `quoteIncrement` | `string` | Amount step on the QUOTE leg — e.g. `"1"` on a CLP market, which settles in whole pesos. Equivalent to Coinbase's `quote_increment`. Ej.: `1`. | | `tradingDays` | `number[]` \| `null` | Days of the week this market trades, as ISO weekday numbers (1 = Monday … 7 = Sunday), evaluated in UTC. `null` means it trades every day — which is the case for every crypto-only market. Tokenised-equity markets trade Monday to Friday. Ej.: `1,2,3,4,5`. | ### PaginationMeta | Campo | Tipo | Descripción | |---|---|---| | `page` | `number` | Ej.: `1`. | | `limit` | `number` | Ej.: `25`. | | `totalItems` | `number` | Ej.: `100`. | | `totalPages` | `number` | Ej.: `4`. | ### PriceResource | Campo | Tipo | Descripción | |---|---|---| | `market` | `string` | The market this price is for, echoed from the request path. Ej.: `BTC-CLP`. | | `side` | `BUY` \| `SELL` | The side priced. Buying and selling the same market are different prices — the spread is real — so a price is only meaningful together with its side. Ej.: `BUY`. | | `rate` | `string` | Quote asset per one unit of base asset. A decimal string. This is the rate for `baseAmount` specifically: Skipo prices a dealer spread, so the rate is a function of size and does NOT scale linearly to a larger amount. Ej.: `59700000`. | | `baseAmount` | `string` | Base-asset amount this price was calculated for. When the request omitted `amount`, this is the market's minimum — not a limit-free rate. Ej.: `0.2`. | | `quoteAmount` | `string` | Quote-asset amount corresponding to `baseAmount` at `rate`. Ej.: `11940000`. | | `indicative` | `boolean` | Always `true`. Present so a client never has to infer non-bindingness from missing fields. This price is not reserved, not held, and cannot be executed — obtaining a confirmable quote is `POST /v2/quotes`, which requires the `trading:write` scope. Ej.: `true`. | | `pricedAt` | `string` | When Skipo calculated this price, ISO-8601. There is no expiry because there is nothing to expire; treat the price as a point-in-time observation and re-read it when it matters. Ej.: `2026-07-30T04:36:02.451Z`. | ### Problem | Campo | Tipo | Descripción | |---|---|---| | `type` | `string` | Stable problem-type URI. Resolves to docs for this error. Ej.: `https://docs.skipo.com/errors/rate_limited`. | | `title` | `string` | Short, human-readable summary (stable, English). Ej.: `Rate limit exceeded`. | | `status` | `number` | HTTP status code. Ej.: `429`. | | `code` | `string` | Stable machine-readable error code (equals the last path segment of `type`). Ej.: `rate_limited`. | | `retryable` | `boolean` | Whether retrying the identical request may succeed. Ej.: `true`. | | `detail` *(opcional)* | `string` | Human-readable, possibly localized detail. | | `instance` *(opcional)* | `string` | The request path that produced the error. | | `traceId` *(opcional)* | `string` | Trace id — joins BigQuery api_request and Cloud Logging. | --- Volver a la [referencia de endpoints](/api). Ver también: [Paginación](/concepts/pagination) · [Errores](/concepts/errors) · [Ids y correlación](/concepts/ids-and-correlation). --- ## Orders {/* AUTO-GENERATED by scripts/gen-api-reference.mjs — do not edit; run `yarn gen:api`. */} # Orders ## `GET /v2/fills` List fills Every fill on the account, newest first, across all orders. Filter by `orderId`, `market`, asset (either leg), side or date range. Cursor-paginated: follow `pagination.nextCursor` until it is null, and do not infer the end from a short page. **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `limit` | query | `integer` | no | Default: `25`. | | `cursor` | query | `string` | no | Opaque cursor from the previous page's `pagination.nextCursor`. | | `orderId` | query | `string` | no | Restrict to the fills of one order (its public id). | | `market` | query | `string` | no | Filter to one market, by the `id` published by `GET /v2/markets` — the same value this endpoint returns as `market`. | | `assetSymbol` | query | `string` | no | Matches either leg — fills that touched this asset. | | `side` | query | `BUY` \| `SELL` | no | | | `startDate` | query | `string` | no | ISO-8601 start date (inclusive). | | `endDate` | query | `string` | no | ISO-8601 end date (exclusive). | | `assetFormat` | query | `base` \| `rebased` | no | How to express tokenised-equity (xStock) amounts. `rebased` (default) = underlying equity (real shares); `base` = settled SPV tokens, i.e. what moved on chain and in the ledger. No effect on other assets. Default: `rebased`. | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | `data`: [`FillResource`](#fillresource)[] · `pagination`: [`CursorMeta`](#cursormeta) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `GET /v2/orders` List orders **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `page` | query | `integer` | no | Default: `1`. | | `limit` | query | `integer` | no | Default: `25`. | | `status` | query | `NEW` \| `PARTIALLY_FILLED` \| `FILLED` \| `FAILED` | no | | | `baseAsset` | query | `string` | no | Filter by base asset code. | | `quoteAsset` | query | `string` | no | Filter by quote asset code. | | `market` | query | `string` | no | Filter to one market, by the `id` published by `GET /v2/markets` — the same value this endpoint returns as `market`. | | `side` | query | `BUY` \| `SELL` | no | | | `assetFormat` | query | `base` \| `rebased` | no | How to express tokenised-equity (xStock) amounts. `rebased` (default) = underlying equity; `base` = settled SPV tokens. No effect on other assets. Default: `rebased`. | | `startDate` | query | `string` | no | ISO-8601 start date. | | `endDate` | query | `string` | no | ISO-8601 end date. | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | `data`: [`OrderResource`](#orderresource)[] · `pagination`: [`PaginationMeta`](#paginationmeta) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `POST /v2/orders` Place an order (execute a quote) Money movement — requires a Tier-2 signed request (X-API-Key header + signed Authorization JWT), not a bare bearer key. Executes the quote identified by `orderId`. Returns 201 Created with `status: "FILLED"` when the balance move is booked, or 202 Accepted with `status: "PROCESSING"` when the trade executed but the balance credit/debit is still being reconciled internally. On 202 the order is accepted — poll GET /v2/orders/{id} for the final state and do NOT retry (a retry places a new order and double-executes). **Autenticación:** **Petición firmada (Tier-2)** **Cuerpo de la petición** — [`PlaceOrderDto`](#placeorderdto) **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `201` | Order placed and filled — the balance move is booked (`status: "filled"`). | [`PlacedOrderResource`](#placedorderresource) | | `202` | Order accepted; settlement is pending (`status: "PROCESSING"`). The trade executed but the balance move is still reconciling internally — poll GET /v2/orders/{id}; do NOT retry. | [`PlacedOrderResource`](#placedorderresource) | | `400` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`validation_error`](https://docs.skipo.com/errors/validation_error) (not retryable). | — | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable), [`invalid_signature`](https://docs.skipo.com/errors/invalid_signature) (not retryable), [`clock_skew`](https://docs.skipo.com/errors/clock_skew) (retryable), [`nonce_reused`](https://docs.skipo.com/errors/nonce_reused) (retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable), [`two_factor_required`](https://docs.skipo.com/errors/two_factor_required) (not retryable). | — | | `422` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unprocessable`](https://docs.skipo.com/errors/unprocessable) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `GET /v2/orders/{id}` Get an order by id **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `id` | path | `string` | sí | | | `assetFormat` | query | `base` \| `rebased` | no | How to express tokenised-equity (xStock) amounts. `rebased` (default) = underlying equity (real shares); `base` = settled SPV tokens, i.e. what moved on chain and in the ledger. No effect on other assets. Default: `rebased`. | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | [`OrderResource`](#orderresource) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `404` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`not_found`](https://docs.skipo.com/errors/not_found) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `GET /v2/orders/{id}/fills` List the fills of an order The fills of one order, newest first. Cursor-paginated: follow `pagination.nextCursor` until it is null. A normal convert has exactly one fill; an on-credit (capacity) order is filled by several as the debt is paid down. **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `id` | path | `string` | sí | | | `limit` | query | `integer` | no | Default: `25`. | | `cursor` | query | `string` | no | Opaque cursor from the previous page's `pagination.nextCursor`. | | `assetFormat` | query | `base` \| `rebased` | no | How to express tokenised-equity (xStock) amounts. `rebased` (default) = underlying equity (real shares); `base` = settled SPV tokens, i.e. what moved on chain and in the ledger. No effect on other assets. Default: `rebased`. | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | `data`: [`FillResource`](#fillresource)[] · `pagination`: [`CursorMeta`](#cursormeta) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `404` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`not_found`](https://docs.skipo.com/errors/not_found) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `POST /v2/quotes` Create a conversion quote Creates a short-lived, single-use CONFIRMABLE quote; execute it via POST /v2/orders (a signed request). Requires the write scope because the call mints server-side state that an order later consumes, and is balance- and capacity-gated — not because funds move here. It is bearer-authenticated rather than signed precisely because the quote itself moves no money. For a NON-BINDING price with no confirmation step and no write scope, use GET /v2/markets/{market}/price. **Cuerpo de la petición** — [`CreateQuoteDto`](#createquotedto) **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `201` | Creado. | [`QuoteResource`](#quoteresource) | | `400` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`validation_error`](https://docs.skipo.com/errors/validation_error) (not retryable). | — | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `422` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unprocessable`](https://docs.skipo.com/errors/unprocessable) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## Esquemas ### CreateQuoteDto | Campo | Tipo | Descripción | |---|---|---| | `baseAsset` | `string` | Base asset code of the market. Together with `quoteAsset` it must name a market `GET /v2/markets` lists — the pairing is its `id`, and it is DIRECTIONAL, so the legs cannot be swapped. An unknown pairing is rejected before anything is priced. Ej.: `BTC`. | | `quoteAsset` | `string` | Quote asset code of the market. Ej.: `CLP`. | | `amountAsset` | `string` | Which leg `amount` is denominated in — it must be `baseAsset` or `quoteAsset`, and anything else is rejected. It selects which minimum applies: `minBaseAmount` or `minQuoteAmount` on `GET /v2/markets`. Ej.: `BTC`. | | `side` | `BUY` \| `SELL` | | | `amount` | `string` | Amount to convert, denominated in `amountAsset`. A decimal string. Ej.: `0.5`. | ### CursorMeta | Campo | Tipo | Descripción | |---|---|---| | `count` | `number` | Number of items in THIS page. Ej.: `25`. | | `nextCursor` | `string` \| `null` | Opaque token for the next page, or null on the last page. Pass it back as `cursor`. Treat it as opaque — its encoding is not part of the contract and may change. Ej.: `MjAyNi0wNy0yNiAxNzo0MjowMS4wMDMzMDl8OWY4Zi00YQ`. | ### FillOrderState | Campo | Tipo | Descripción | |---|---|---| | `status` | `NEW` \| `PARTIALLY_FILLED` \| `FILLED` \| `FAILED` | | | `filledBaseAmount` | `string` | Cumulative filled base amount on the order. Positive magnitude. | | `filledQuoteAmount` | `string` | Cumulative filled quote amount on the order. Positive magnitude. | ### FillResource | Campo | Tipo | Descripción | |---|---|---| | `id` | `string` | Public fill id. This is the same value the ledger entry carries as `source.id`, so a fill and its ledger movements join with no extra lookup. | | `orderId` | `string` | Id of the order this fill belongs to. | | `sequence` | `number` | 1-based position of this fill within its order. A normal convert has exactly one fill; an on-credit (capacity) order has several. Ej.: `1`. | | `side` | `string` | Ej.: `BUY`. | | `market` | `string` | Ej.: `BTC-CLP`. | | `baseAsset` | `string` | | | `quoteAsset` | `string` | | | `baseAmount` | `string` | Base amount filled by this fill. Positive magnitude. | | `quoteAmount` | `string` | Quote amount filled by this fill. Positive magnitude. | | `rate` | `string` | | | `multiplier` | `string` | Rebase factor applied to `baseAmount` and `rate`, as it stood AT THIS FILL — not today. "1" when no scaling applies. Divide `baseAmount` by it to recover the settled (SPV) figure, which is what moved on chain and in the ledger. Ej.: `1`. | | `onCredit` | `boolean` | Whether the owning order was funded on credit (capacity). | | `executedAt` | `string` | | | `order` | [`FillOrderState`](#fillorderstate) | | ### OrderResource | Campo | Tipo | Descripción | |---|---|---| | `id` | `string` | Public order id (the client order id). | | `status` | `NEW` \| `PARTIALLY_FILLED` \| `FILLED` \| `FAILED` | | | `side` | `string` | Ej.: `BUY`. | | `market` | `string` | Ej.: `BTC-CLP`. | | `baseAsset` | `string` | | | `quoteAsset` | `string` | | | `baseAmount` | `string` | Ordered base amount. Positive magnitude. | | `filledBaseAmount` | `string` | Cumulative filled base amount. Positive magnitude. | | `quoteAmount` | `string` | Ordered quote amount. Positive magnitude. | | `filledQuoteAmount` | `string` | Cumulative filled quote amount. Positive magnitude. | | `rate` | `string` | | | `multiplier` | `string` | Rebase factor applied to the base amounts and rate, as it stood when the order was placed. "1" when no scaling applies. Divide a base amount by it to recover the settled (SPV) figure. Ej.: `1`. | | `onCredit` | `boolean` | Whether the order was funded on credit (capacity). | | `createdAt` | `string` | | ### PaginationMeta | Campo | Tipo | Descripción | |---|---|---| | `page` | `number` | Ej.: `1`. | | `limit` | `number` | Ej.: `25`. | | `totalItems` | `number` | Ej.: `100`. | | `totalPages` | `number` | Ej.: `4`. | ### PlaceOrderDto | Campo | Tipo | Descripción | |---|---|---| | `orderId` | `string` | The orderId of the quote to execute (returned by POST /v2/quotes). Ej.: `ord_abc123`. | ### PlacedOrderResource | Campo | Tipo | Descripción | |---|---|---| | `id` | `string` | Public order id (the client order id). | | `status` | `FILLED` \| `PROCESSING` | Placement outcome. `FILLED` — the fill executed and its balance move is booked. `PROCESSING` — the fill executed but the balance credit/debit is still being reconciled internally; the order is accepted, not yet booked. Poll GET /v2/orders/{id} for the final state, and do NOT retry a `PROCESSING` order (a retry places a new order and double-executes). | | `transactionId` *(opcional)* | `string` | Id of the first fill, when the order fills at placement (balance orders). | ### Problem | Campo | Tipo | Descripción | |---|---|---| | `type` | `string` | Stable problem-type URI. Resolves to docs for this error. Ej.: `https://docs.skipo.com/errors/rate_limited`. | | `title` | `string` | Short, human-readable summary (stable, English). Ej.: `Rate limit exceeded`. | | `status` | `number` | HTTP status code. Ej.: `429`. | | `code` | `string` | Stable machine-readable error code (equals the last path segment of `type`). Ej.: `rate_limited`. | | `retryable` | `boolean` | Whether retrying the identical request may succeed. Ej.: `true`. | | `detail` *(opcional)* | `string` | Human-readable, possibly localized detail. | | `instance` *(opcional)* | `string` | The request path that produced the error. | | `traceId` *(opcional)* | `string` | Trace id — joins BigQuery api_request and Cloud Logging. | ### QuoteResource | Campo | Tipo | Descripción | |---|---|---| | `clientOrderId` | `string` | DEPRECATED alias of `orderId`, carrying the same value. Use `orderId`. Kept for one release so a client reading this field does not break. | | `orderId` | `string` | The id of this quote — pass it to POST /v2/orders to execute. It is also the `id` the resulting order will carry, and the value `GET /v2/orders`, `/v2/orders/{id}/fills`, `GET /v2/fills` and `GET /v2/ledger?orderId=` all key on. One id, learned once, used through the whole flow. | | `market` | `string` | The market this quote priced, spelled exactly as `GET /v2/markets` publishes it — so it can be passed straight back to `GET /v2/markets/{market}`. Without it a stored quote is not self-describing: the numbers below mean nothing without knowing what was priced. This is the market Skipo RESOLVED, which is authoritative over the `baseAsset`/`quoteAsset` you sent — those are matched case-insensitively, so the casing here may differ from your request. Ej.: `BTC-CLP`. | | `rate` | `string` | Quoted exchange rate. | | `baseAmount` | `string` | Quoted base amount. Positive magnitude. | | `quoteAmount` | `string` | Quoted quote amount. Positive magnitude. | | `quotedAt` | `string` \| `null` | When the quote was produced, ISO-8601. `null` if the upstream time was unusable. Ej.: `2026-07-28T12:34:56.789Z`. | | `expiresAt` | `string` \| `null` | When this quote stops being confirmable, ISO-8601 — about five seconds after `quotedAt`. Execute it with POST /v2/orders before this instant; afterwards expect `quotation_expired` and request a fresh quote. `null` means the expiry could not be determined — treat that as unknown, never as already expired. Ej.: `2026-07-28T12:35:01.789Z`. | --- Volver a la [referencia de endpoints](/api). Ver también: [Paginación](/concepts/pagination) · [Errores](/concepts/errors) · [Ids y correlación](/concepts/ids-and-correlation). --- ## System {/* AUTO-GENERATED by scripts/gen-api-reference.mjs — do not edit; run `yarn gen:api`. */} # System ## `GET /v2/health` Service health Always answers `200`, even during maintenance — the body carries the state. A `MAINTENANCE` status means every other route is returning `503`, so poll this to know when to resume instead of probing a real endpoint. **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | [`HealthResponse`](#healthresponse) | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | --- ## `GET /v2/time` Server time for request signing and clock synchronization **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | [`TimeResponse`](#timeresponse) | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | --- ## Esquemas ### HealthResponse | Campo | Tipo | Descripción | |---|---|---| | `status` | `UP` \| `MAINTENANCE` | `UP` when serving normally. `MAINTENANCE` when the API is in maintenance — every other route is returning `503` with the `maintenance` problem type, so retry later rather than treating it as an outage. Ej.: `UP`. | ### Problem | Campo | Tipo | Descripción | |---|---|---| | `type` | `string` | Stable problem-type URI. Resolves to docs for this error. Ej.: `https://docs.skipo.com/errors/rate_limited`. | | `title` | `string` | Short, human-readable summary (stable, English). Ej.: `Rate limit exceeded`. | | `status` | `number` | HTTP status code. Ej.: `429`. | | `code` | `string` | Stable machine-readable error code (equals the last path segment of `type`). Ej.: `rate_limited`. | | `retryable` | `boolean` | Whether retrying the identical request may succeed. Ej.: `true`. | | `detail` *(opcional)* | `string` | Human-readable, possibly localized detail. | | `instance` *(opcional)* | `string` | The request path that produced the error. | | `traceId` *(opcional)* | `string` | Trace id — joins BigQuery api_request and Cloud Logging. | ### TimeResponse | Campo | Tipo | Descripción | |---|---|---| | `serverTime` | `number` | Server time, milliseconds since Unix epoch. Ej.: `1783003522123`. | | `iso` | `string` | The same instant as ISO-8601 UTC. Ej.: `2026-07-28T12:34:56.789Z`. | --- Volver a la [referencia de endpoints](/api). Ver también: [Paginación](/concepts/pagination) · [Errores](/concepts/errors) · [Ids y correlación](/concepts/ids-and-correlation). --- ## Webhooks {/* AUTO-GENERATED by scripts/gen-api-reference.mjs — do not edit; run `yarn gen:api`. */} # Webhooks ## `GET /v2/.well-known/webhook-jwks.json` Webhook signing JWKS Public JSON Web Key Set for verifying the EdDSA signature on webhook deliveries. Select the key by the `kid` in the delivery signature header. **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | JWKS document — `{ "keys": [...] }`. | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | --- ## Esquemas ### Problem | Campo | Tipo | Descripción | |---|---|---| | `type` | `string` | Stable problem-type URI. Resolves to docs for this error. Ej.: `https://docs.skipo.com/errors/rate_limited`. | | `title` | `string` | Short, human-readable summary (stable, English). Ej.: `Rate limit exceeded`. | | `status` | `number` | HTTP status code. Ej.: `429`. | | `code` | `string` | Stable machine-readable error code (equals the last path segment of `type`). Ej.: `rate_limited`. | | `retryable` | `boolean` | Whether retrying the identical request may succeed. Ej.: `true`. | | `detail` *(opcional)* | `string` | Human-readable, possibly localized detail. | | `instance` *(opcional)* | `string` | The request path that produced the error. | | `traceId` *(opcional)* | `string` | Trace id — joins BigQuery api_request and Cloud Logging. | --- Volver a la [referencia de endpoints](/api). Ver también: [Paginación](/concepts/pagination) · [Errores](/concepts/errors) · [Ids y correlación](/concepts/ids-and-correlation). --- ## Withdrawals {/* AUTO-GENERATED by scripts/gen-api-reference.mjs — do not edit; run `yarn gen:api`. */} # Withdrawals ## `GET /v2/withdrawals` List withdrawals **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `page` | query | `integer` | no | Default: `1`. | | `limit` | query | `integer` | no | Default: `25`. | | `assetSymbol` | query | `string` | no | | | `status` | query | `string` | no | | | `subType` | query | `INTERNAL` \| `EXTERNAL_CRYPTO` \| `EXTERNAL_FIAT_BANK` \| `EXTERNAL_FIAT_REDPAY` \| `NETWORK_FEE` \| `TRANSFER_FEE` | no | | | `assetFormat` | query | `base` \| `rebased` | no | How to express tokenised-equity (xStock) amounts. `rebased` (default) = underlying equity; `base` = settled SPV tokens. No effect on other assets. Default: `rebased`. | | `startDate` | query | `string` | no | ISO-8601 start date. | | `endDate` | query | `string` | no | ISO-8601 end date. | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | `data`: [`WithdrawalResource`](#withdrawalresource)[] · `pagination`: [`PaginationMeta`](#paginationmeta) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `POST /v2/withdrawals` Create a withdrawal Money movement — requires a Tier-2 signed request (X-API-Key header + signed Authorization JWT), not a bare bearer key. **Autenticación:** **Petición firmada (Tier-2)** **Cuerpo de la petición** — [`CreateWithdrawalDto`](#createwithdrawaldto) **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `201` | Creado. | [`WithdrawalResource`](#withdrawalresource) | | `400` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`validation_error`](https://docs.skipo.com/errors/validation_error) (not retryable). | — | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable), [`invalid_signature`](https://docs.skipo.com/errors/invalid_signature) (not retryable), [`clock_skew`](https://docs.skipo.com/errors/clock_skew) (retryable), [`nonce_reused`](https://docs.skipo.com/errors/nonce_reused) (retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable), [`two_factor_required`](https://docs.skipo.com/errors/two_factor_required) (not retryable). | — | | `422` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unprocessable`](https://docs.skipo.com/errors/unprocessable) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## `GET /v2/withdrawals/{id}` Get a withdrawal by id **Parámetros** | Parámetro | En | Tipo | Requerido | Descripción | |---|---|---|---|---| | `id` | path | `string` | sí | | **Respuestas** | Código | Descripción | Cuerpo | |---|---|---| | `200` | Éxito. | [`WithdrawalResource`](#withdrawalresource) | | `401` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`unauthorized`](https://docs.skipo.com/errors/unauthorized) (not retryable), [`key_expired`](https://docs.skipo.com/errors/key_expired) (not retryable), [`key_revoked`](https://docs.skipo.com/errors/key_revoked) (not retryable). | — | | `403` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`insufficient_scope`](https://docs.skipo.com/errors/insufficient_scope) (not retryable), [`ip_not_allowed`](https://docs.skipo.com/errors/ip_not_allowed) (not retryable), [`api_access_disabled`](https://docs.skipo.com/errors/api_access_disabled) (not retryable). | — | | `404` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`not_found`](https://docs.skipo.com/errors/not_found) (not retryable). | — | | `429` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`rate_limited`](https://docs.skipo.com/errors/rate_limited) (retryable). | — | | `500` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`internal_error`](https://docs.skipo.com/errors/internal_error) (retryable). | — | | `502` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`downstream_unavailable`](https://docs.skipo.com/errors/downstream_unavailable) (retryable). | — | | `503` | `application/problem+json` (RFC 9457) — branch on `code`, not on the status: [`maintenance`](https://docs.skipo.com/errors/maintenance) (retryable). | — | --- ## Esquemas ### CreateWithdrawalDto | Campo | Tipo | Descripción | |---|---|---| | `assetSymbol` | `string` | Asset to withdraw. Ej.: `BTC`. | | `amount` | `string` | Amount to withdraw, as a positive decimal string. The withdrawal fee is charged on top — the balance is debited `amount` + `fee`. Ej.: `0.05`. | | `contactId` *(opcional)* | `string` | Destination contact id (a whitelisted address/account). | | `contactReference` *(opcional)* | `string` | Destination contact reference/alias. | ### PaginationMeta | Campo | Tipo | Descripción | |---|---|---| | `page` | `number` | Ej.: `1`. | | `limit` | `number` | Ej.: `25`. | | `totalItems` | `number` | Ej.: `100`. | | `totalPages` | `number` | Ej.: `4`. | ### Problem | Campo | Tipo | Descripción | |---|---|---| | `type` | `string` | Stable problem-type URI. Resolves to docs for this error. Ej.: `https://docs.skipo.com/errors/rate_limited`. | | `title` | `string` | Short, human-readable summary (stable, English). Ej.: `Rate limit exceeded`. | | `status` | `number` | HTTP status code. Ej.: `429`. | | `code` | `string` | Stable machine-readable error code (equals the last path segment of `type`). Ej.: `rate_limited`. | | `retryable` | `boolean` | Whether retrying the identical request may succeed. Ej.: `true`. | | `detail` *(opcional)* | `string` | Human-readable, possibly localized detail. | | `instance` *(opcional)* | `string` | The request path that produced the error. | | `traceId` *(opcional)* | `string` | Trace id — joins BigQuery api_request and Cloud Logging. | ### WithdrawalResource | Campo | Tipo | Descripción | |---|---|---| | `id` | `string` | Withdrawal id. Use it to re-fetch this withdrawal and to match its ledger entry (`source.id`). | | `type` | `string` | Ej.: `WITHDRAWAL`. | | `subType` | `INTERNAL` \| `EXTERNAL_CRYPTO` \| `EXTERNAL_FIAT_BANK` \| `EXTERNAL_FIAT_REDPAY` \| `NETWORK_FEE` \| `TRANSFER_FEE` | | | `assetSymbol` | `string` | Asset code of the withdrawal. Ej.: `BTC`. | | `amount` | `string` | Amount withdrawn, excluding the fee. Positive magnitude, in `assetSymbol`. | | `fee` | `string` | The withdrawal fee charged by Skipo, in `assetSymbol`. A flat fee — NOT the blockchain network fee, which Skipo absorbs. Positive magnitude. | | `total` | `string` | Total debited from the balance (`amount` + `fee`). Positive magnitude, in `assetSymbol`. | | `status` | `string` | | | `createdAt` | `string` | | | `withdrawalData` | `object` | Sub-type-specific details. | --- Volver a la [referencia de endpoints](/api). Ver también: [Paginación](/concepts/pagination) · [Errores](/concepts/errors) · [Ids y correlación](/concepts/ids-and-correlation). --- ## Autenticación import Tabs from '@theme/Tabs' import TabItem from '@theme/TabItem' # Autenticación La API de Skipo usa un esquema de **dos niveles**. El nivel depende de la sensibilidad de la operación: - **Nivel 1 — llave _bearer_:** lecturas y escrituras no sensibles. - **Nivel 2 — JWT firmado por petición:** movimiento de dinero. ## Las tres piezas de una llave Una llave pone en juego **tres** valores, y van en sitios distintos. Sólo los dos primeros los emite Skipo; el tercero lo generas tú y su mitad privada nunca sale de tu lado. | Valor | Va en | ¿Secreto? | Origen | |---|---|---|---| | **Secreto de la llave** | `Authorization: Bearer ` (Nivel 1) | Sí — se muestra **una sola vez** | Lo emite Skipo al crear la llave | | **Prefijo** | Cabecera `X-API-Key` y claim `sub` del JWT (Nivel 2) | No, es público | Lo emite Skipo — y es **derivable** del secreto | | **Clave de firma** | Firma el JWT de Nivel 2 | Sí — la privada nunca se envía | La generas tú; a Skipo le subes **sólo la pública** (SPKI) | :::info[El prefijo son los 21 primeros caracteres del secreto] El secreto es `skp_live_` + 32 caracteres aleatorios + 6 de _checksum_. El prefijo es `skp_live_` + los **12 primeros** de esos 32 — es decir, exactamente los 21 primeros caracteres del secreto: ``` skp_live_aB3dE5gH7jK9mN2pQ4rS6tU8vW0xY1zA9bC3dE ← secreto (Nivel 1) skp_live_aB3dE5gH7jK9 ← prefijo (Nivel 2, público) ``` No necesitas guardarlo aparte: puedes derivarlo del secreto cuando lo necesites. ::: ## Nivel 1 — Llave _bearer_ La mayoría de las operaciones se autentican con una llave secreta en la cabecera `Authorization`: ```bash curl https://api.skipo.com/v2/balances \ -H "Authorization: Bearer $SKIPO_BEARER_KEY" ``` ```js const res = await fetch(`${base}/v2/balances`, { headers: { Authorization: `Bearer ${process.env.SKIPO_BEARER_KEY}` }, }) console.log(await res.json()) ``` ```python import os, urllib.request, json req = urllib.request.Request( f"{base}/v2/balances", headers={"Authorization": f"Bearer {os.environ['SKIPO_BEARER_KEY']}"}, ) with urllib.request.urlopen(req) as resp: print(json.load(resp)) ``` Sobre las llaves _bearer_: - Formato `skp_live_…` / `skp_test_…`: 32 caracteres aleatorios (`0-9A-Za-z`) generados con un CSPRNG, más un _checksum_ de 6 que permite descartar una llave mal copiada sin llamar a la API. - Se muestran **una sola vez** al crearlas; Skipo almacena solo su hash SHA-256. - Cada llave tiene **scopes** que limitan a qué operaciones accede (ver abajo). - Cada llave admite, de forma **opcional**, una **lista de IPs permitidas**. Si la defines, las peticiones desde cualquier otro origen se rechazan con [`ip_not_allowed`](/errors/ip_not_allowed), tanto en el nivel _bearer_ como en el firmado. Si no la defines, no se aplica ninguna restricción por IP. ## Nivel 2 — JWT firmado por petición **Sólo estas dos operaciones exigen firma. Todas las demás son de Nivel 1:** | Operación | Endpoint | Scope | |---|---|---| | Ejecutar una cotización | `POST /v2/orders` | `trading:write` | | Crear un retiro | `POST /v2/withdrawals` | `transfers:write` | :::warning[«Escritura» no implica firma] El nivel lo determina el **movimiento de dinero**, no el verbo HTTP. Por ejemplo, `PATCH /v2/contacts/{contactId}` sólo edita un alias: es de **Nivel 1** y se autentica con la llave _bearer_. Lo mismo aplica a `POST /v2/quotes`, que cotiza pero no ejecuta. Si firmas un endpoint de Nivel 1, la API responde `401 unauthorized` con `reason: "signed_jwt_on_bearer_route"`. Tu llave está bien — lo que sobra es la firma. Reenvía la petición con `Authorization: Bearer ` y sin `X-API-Key`. ::: Estas operaciones exigen, en lugar de la llave _bearer_, dos cabeceras: el **prefijo público** de tu llave y un **JWT firmado** que prueba que tú generaste _esa_ petición concreta y que nadie la alteró: ``` X-API-Key: skp_live_aB3dE5gH7jK9 # el PREFIJO (21 caracteres), no el secreto Authorization: Bearer # el JWT, no el secreto de la llave ``` :::caution[Ninguna de las dos cabeceras lleva el secreto de la llave] En Nivel 2 el secreto no viaja: `X-API-Key` lleva el **prefijo** y `Authorization` lleva el **JWT**. Si pones el secreto completo en `X-API-Key`, no resuelve ninguna llave y recibes `401` [`unauthorized`](/errors/unauthorized) con `detail: "Unknown API key."` — no `invalid_signature`. Cuando algo falla en la firma, la respuesta trae un campo `reason` que nombra la primera comprobación que falló ([tabla completa](/errors/invalid_signature)). ::: El JWT se firma con tu **clave privada** (Ed25519 por defecto, `RS256` como alternativa) e incluye estos claims: | Claim | Valor | |---|---| | `sub` | El **prefijo** de la llave (idéntico al valor de `X-API-Key`). | | `uri` | `"MÉTODO /ruta?query"` — método, un espacio, y la ruta con su query **exactamente** como se envía (p. ej. `"POST /v2/withdrawals"`). | | `nonce` | Valor único por petición (p. ej. un UUID v4). Previene _replay_. | | `iat` | Emitido en (epoch, segundos). | | `exp` | Expira en. Debe cumplir **`exp − iat ≤ 60`**. | | `bodyHash` | **SHA-256 en hex** de los **bytes crudos** del cuerpo. | :::warning[Cuerpo crudo] El `bodyHash` se calcula sobre los bytes crudos del cuerpo, exactamente como se transmiten. Serializa el cuerpo **una sola vez**, calcula el hash sobre esos bytes, y envía **esos mismos bytes**. Si reserializas el JSON después de firmar, el `bodyHash` deja de coincidir y la petición se rechaza con [`invalid_signature`](/errors/invalid_signature). ::: ### Ejemplo — firmar un retiro ```js import { SignJWT, importPKCS8 } from 'jose' import { createHash, randomUUID } from 'node:crypto' const method = 'POST' const path = '/v2/withdrawals' // Serializa el cuerpo UNA vez; estos bytes se hashean y se envían. const body = JSON.stringify({ assetSymbol: 'BTC', amount: '0.05', contactId }) const bodyHash = createHash('sha256').update(Buffer.from(body, 'utf8')).digest('hex') const now = Math.floor(Date.now() / 1000) const key = await importPKCS8(process.env.SKIPO_PRIVATE_KEY_PEM, 'EdDSA') const jwt = await new SignJWT({ uri: `${method} ${path}`, nonce: randomUUID(), bodyHash }) .setProtectedHeader({ alg: 'EdDSA' }) .setSubject(keyPrefix) // === X-API-Key .setIssuedAt(now) .setExpirationTime(now + 55) // exp − iat ≤ 60 .sign(key) const res = await fetch(base + path, { method, headers: { 'X-API-Key': keyPrefix, Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json', }, body, // los MISMOS bytes cubiertos por bodyHash }) ``` ```python import hashlib, json, time, uuid, jwt from cryptography.hazmat.primitives.serialization import load_pem_private_key method, path = "POST", "/v2/withdrawals" # Serializa el cuerpo UNA vez; estos bytes se hashean y se envían. body = json.dumps({"assetSymbol": "BTC", "amount": "0.05", "contactId": contact_id}).encode() body_hash = hashlib.sha256(body).hexdigest() now = int(time.time()) private_key = load_pem_private_key(os.environ["SKIPO_PRIVATE_KEY_PEM"].encode(), password=None) token = jwt.encode( { "sub": key_prefix, # === X-API-Key "uri": f"{method} {path}", "nonce": str(uuid.uuid4()), "iat": now, "exp": now + 55, # exp − iat ≤ 60 "bodyHash": body_hash, }, private_key, algorithm="EdDSA", ) # Envía body como los MISMOS bytes cubiertos por bodyHash, con: # X-API-Key: key_prefix + Authorization: Bearer ``` :::note Versiones completas y ejecutables de estos ejemplos están en el directorio [`examples/`](https://gitlab.com/skipo-engine/apps/api-public-docs/-/tree/main/examples). ::: ## Generar el par de llaves La clave privada de firma **siempre la generas tú**: Skipo sólo recibe la pública (SPKI) y nunca ve la privada. Hay dos formas de hacerlo, y la diferencia importa. Sea cual sea la que elijas, puedes registrar la clave pública **al crear la llave** —ambas quedan guardadas en la misma operación— o añadirla después a una llave que ya existe. ### Opción A — en tu máquina (recomendada) La privada no pasa nunca por un navegador. ```bash # Clave privada (Ed25519, PKCS#8) — guárdala en secreto openssl genpkey -algorithm ed25519 -out skipo-signing-key.pem # Clave pública (SPKI) — esta es la que subes al panel openssl pkey -in skipo-signing-key.pem -pubout -out skipo-signing-key.pub.pem ``` :::note[macOS] El `openssl` del sistema en macOS es LibreSSL y no soporta `-algorithm ed25519`. Instala OpenSSL con Homebrew (`brew install openssl`) o genera el par con la utilidad de tu lenguaje (`crypto.generateKeyPairSync('ed25519')` en Node, `cryptography` en Python). ::: ### Opción B — en el navegador, desde el panel El panel puede generar el par por ti con la **Web Crypto API** del navegador. La pública se sube y la privada se te muestra **una sola vez** para que la guardes; no se transmite ni se almacena en Skipo. El compromiso: la privada existe en la memoria de la página mientras dura el proceso, así que hereda la seguridad de ese navegador y sus extensiones. Es la vía cómoda para empezar o para una llave `skp_test_`; para llaves `skp_live_` que mueven dinero, prefiere la Opción A. :::note[Soporte del navegador] Ed25519 en Web Crypto no está en todos los navegadores. El panel lo comprueba antes de ofrecer esta opción; si no está disponible, usa la Opción A. ::: ## Relojes y `/v2/time` Las peticiones firmadas son sensibles al desfase de reloj: si tu `iat` va muy por delante de la hora del servidor, la petición se rechaza con [`clock_skew`](/errors/clock_skew) (reintentable). Consulta la hora del servidor con `GET /v2/time` y sincroniza antes de firmar si sospechas de un desfase. ## Rotación de llaves Los dos niveles rotan de forma distinta. **Llave _bearer_ — período de gracia de 7 días.** Al rotarla, Skipo emite un secreto nuevo y el anterior **sigue funcionando durante 7 días**; pasado ese plazo devuelve [`key_expired`](/errors/key_expired). Despliega el secreto nuevo dentro de esa ventana. **Llave de firma — sin período de gracia.** Una llave de firma pasa de activa a revocada de inmediato: al revocarla deja de verificar en el acto. La superposición la controlas tú. El verificador prueba **todas** tus llaves de firma activas, así que sube la clave pública nueva **junto a** la actual: ambas verifican en paralelo mientras las dos sigan activas. Migra tu firma a la nueva y recién entonces revoca la anterior. ## Errores de autenticación | Situación | `code` | |---|---| | Falta o es desconocida la credencial | [`unauthorized`](/errors/unauthorized) | | Firma o claim inválidos | [`invalid_signature`](/errors/invalid_signature) | | Reloj adelantado | [`clock_skew`](/errors/clock_skew) | | `nonce` reutilizado | [`nonce_reused`](/errors/nonce_reused) | | Falta un scope | [`insufficient_scope`](/errors/insufficient_scope) | | IP no permitida | [`ip_not_allowed`](/errors/ip_not_allowed) | ## Scopes Los scopes se fijan al crear la llave y acotan lo que puede hacer: | Scope | Permite | |---|---| | `accounts:read` | Leer cuenta, balances y movimientos. | | `market_data:read` | Leer datos de referencia (monedas, mercados). | | `transfers:read` | Leer retiros. | | `transfers:write` | Crear retiros (requiere firma). | | `trading:read` | Leer conversiones y órdenes. | | `trading:write` | Pedir cotizaciones y confirmar conversiones (confirmar requiere firma). | | `contacts:read` | Leer contactos. | | `contacts:write` | Editar el alias/referencia de un contacto. | | `webhooks:read` | **Reservado — todavía no utilizable.** Hoy ningún endpoint lo exige: la gestión de webhooks es solo desde el panel. Marcarlo en una llave no habilita nada. | --- ## Errores La API devuelve errores en formato **`application/problem+json`** ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)). Todo error comparte una estructura estable y predecible: ```json { "type": "https://docs.skipo.com/errors/unauthorized", "title": "Unauthorized", "status": 401, "detail": "This operation requires a signed request (X-API-Key header + signed JWT).", "code": "unauthorized", "retryable": false, "instance": "/v2/withdrawals" } ``` ## Campos | Campo | Descripción | |---|---| | `type` | URI estable que documenta el error (apunta a `docs.skipo.com/errors/{code}`). | | `title` | Resumen legible del tipo de error. | | `status` | Código HTTP. | | `detail` | Descripción específica de _esta_ ocurrencia. | | `code` | **Código estable** legible por máquina — úsalo para ramificar tu lógica. | | `retryable` | `true` si reintentar la misma petición puede tener éxito. | | `instance` | Ruta de la petición que falló. | | `reason` | **Sub-código estable** presente en _algunos_ errores (p. ej. varios `422 unprocessable`) que precisa _qué_ regla de negocio falló — más fino que `code`. Ramificable igual que `code`. Ausente cuando no aplica. | :::tip Ramifica tu manejo de errores sobre `code` (y sobre `reason` cuando esté presente), nunca sobre `title` ni `detail` (que pueden cambiar de redacción o traducirse). `code` y `reason` son contratos estables. ::: Un mismo `code` puede cubrir varias causas; `reason` las distingue. Por ejemplo, un `422 unprocessable` al pedir un precio sobre un mercado cerrado: ```json { "type": "https://docs.skipo.com/errors/unprocessable", "title": "Unprocessable request", "status": 422, "code": "unprocessable", "reason": "MARKET_CLOSED", "detail": "Market 'NVDASPV-CLP' is closed right now.", "retryable": false, "instance": "/v2/markets/NVDASPV-CLP/price?side=BUY" } ``` ## Códigos comunes | `code` | HTTP | ¿Reintentable? | |---|---|---| | [`unauthorized`](/errors/unauthorized) | 401 | no | | [`invalid_signature`](/errors/invalid_signature) | 401 | no | | [`insufficient_scope`](/errors/insufficient_scope) | 403 | no | | [`not_found`](/errors/not_found) | 404 | no | | [`validation_error`](/errors/validation_error) | 400 | no | | [`unprocessable`](/errors/unprocessable) | 422 | no | | [`rate_limited`](/errors/rate_limited) | 429 | sí | | [`internal_error`](/errors/internal_error) | 500 | sí | El **[catálogo completo de errores](/errors)** documenta los 22 códigos, cada uno con su propia página en `docs.skipo.com/errors/{code}` — la misma URI que aparece en el campo `type` de la respuesta. --- ## Ids y correlación Todo recurso de v2 expone un campo **`id`**, y ese mismo valor sirve para volver a consultarlo y para encontrarlo en el libro contable. Esta página es el contrato completo: qué id devuelve cada recurso y cómo saltar de uno a otro. ## La regla en una línea > El `id` de un retiro, depósito o fill es **exactamente** el valor que su asiento contable > publica en `source.id`. Por eso nunca necesitas recorrer el libro buscando: filtras por él. ## Recursos y sus ids | Recurso | `id` | Volver a consultarlo | |---|---|---| | Retiro | id del retiro | `GET /v2/withdrawals/{id}` | | Depósito | id del depósito | `GET /v2/deposits/{id}` | | Orden | id de la orden (tu _client order id_) | `GET /v2/orders/{id}` | | Fill | id del fill | `GET /v2/fills?orderId=…` | | Asiento contable | id del asiento | `GET /v2/ledger/{id}` | ## El asiento contable apunta hacia atrás Cada entrada de `GET /v2/ledger` lleva un objeto **`source`** que dice qué la causó: ```json { "id": "lm_998877", "source": { "type": "withdrawal", "id": "wd_123" }, "type": "WITHDRAWAL", "subType": "EXTERNAL_CRYPTO", "assetSymbol": "USDT", "amount": "150", "fee": "0.5", "total": "150.5", "balance": "849.5", "createdAt": "2026-07-09T14:11:05.501Z" } ``` `source.type` es uno de `withdrawal`, `deposit`, `fill` u `other`, y `source.id` es el `id` de ese recurso. Es el mismo patrón que `balance_transaction.source` de Stripe. ## Los saltos que vas a hacer ### De un retiro o depósito a sus asientos ```bash GET /v2/ledger?assetSymbol=USDT&sourceId=wd_123 ``` Una sola llamada. `sourceId` filtra por el mismo valor que el recurso publica como `id`. ### De una orden a todos sus asientos Una orden se completa con **1..N fills**, y cada fill genera asientos. Para no encadenar consultas, el filtro `orderId` resuelve la cadena entera por ti: ```bash GET /v2/ledger?assetSymbol=CLP&orderId=clord_01H… ``` :::note[Una orden liquida en dos activos] Una conversión mueve dos activos (por ejemplo `BTC` y `CLP`), y `assetSymbol` es obligatorio en el libro contable: selecciona **qué pata** estás mirando. Para ver ambas, haz una llamada por activo. ::: ### De una orden a sus fills ```bash GET /v2/orders/{id}/fills # anidada GET /v2/fills?orderId={id} # equivalente, filtrando la colección global ``` Las dos devuelven el mismo recurso. Existen ambas porque Coinbase y Binance también las tienen y cada cliente alcanza una distinta. Ambas paginan por [cursor](/concepts/pagination). ### De un fill a la orden que lo contiene El fill trae `orderId`, y además **incrusta el estado acumulado de la orden** para que no tengas que releerla después de cada ejecución: ```json { "id": "1096473", "orderId": "clord_01H…", "sequence": 2, "side": "BUY", "market": "BTC-CLP", "baseAmount": "0.004", "quoteAmount": "260000.00", "rate": "65000000.00", "multiplier": "1", "onCredit": true, "executedAt": "2026-07-09T14:07:11.000Z", "order": { "status": "PARTIALLY_FILLED", "filledBaseAmount": "0.008", "filledQuoteAmount": "520000.00" } } ``` `sequence` es la posición del fill dentro de su orden, empezando en 1. ## Órdenes y fills Una **orden** es lo que creas al confirmar una cotización. Un **fill** es cada ejecución contra ella. - Una conversión normal → **exactamente un fill**. - Una orden a crédito (`onCredit: true`) → **varios fills**, a medida que se paga la deuda. Por eso `filledBaseAmount` puede ir por debajo de `baseAmount` durante un tiempo, y por eso el webhook `fill.created` puede llegar varias veces para el mismo `orderId`. ## Convenciones de los importes | Convención | Qué significa | |---|---| | **Magnitudes positivas** | `amount`, `fee` y `total` son siempre positivos en todos los recursos. La dirección la da el tipo de recurso o el `type` del asiento, nunca el signo. | | **`balance` sí lleva signo** | Es un **estado** (el saldo tras el asiento), no un movimiento. No se convierte a magnitud. | | **MAYÚSCULAS** | `status`, `type`, `subType` y `side` son siempre mayúsculas: `FILLED`, `WITHDRAWAL`, `EXTERNAL_CRYPTO`, `BUY`. | | **Decimales como cadena** | Los importes viajan como cadenas para no perder precisión. Parséalos con un tipo decimal, nunca con un `float`. | --- ## Paginación Casi todas las colecciones devuelven la misma envoltura: `data` (los elementos) y `pagination` (los metadatos de navegación). La excepción es `GET /v2/balances`, que devuelve un **arreglo plano**: tienes una posición por activo, así que no hay nada que paginar. Lo que cambia es **qué contiene `pagination`**, porque v2 usa dos estrategias. | Estrategia | Dónde | Metadatos | |---|---|---| | **Offset** (por defecto) | Todo salvo fills | `page`, `limit`, `totalItems`, `totalPages` | | **Cursor** | `GET /v2/fills` y `GET /v2/orders/{id}/fills` | `count`, `nextCursor` | ## Offset — el caso general ```json { "data": [ { "id": "wd_123", "status": "COMPLETED" }, { "id": "wd_124", "status": "PENDING" } ], "pagination": { "page": 1, "limit": 25, "totalItems": 132, "totalPages": 6 } } ``` | Parámetro | Descripción | |---|---| | `page` | Número de página. Empieza en **1**. Por defecto `1`. | | `limit` | Elementos por página. Por defecto **25**, máximo **100**. | ## Cursor — los fills Los dos endpoints de fills paginan por **cursor**, no por offset: ```json { "data": [ { "id": "1096473", "orderId": "clord_01H…", "baseAmount": "0.01" } ], "pagination": { "count": 25, "nextCursor": "MjAyNi0wNy0yNiAxNzo0MjowMS4wMDMzMDl8OWY4Zi00YQ" } } ``` | Parámetro | Descripción | |---|---| | `limit` | Elementos por página. Por defecto **25**, máximo **100**. | | `cursor` | El `pagination.nextCursor` de la página anterior. Omítelo en la primera. | Recorrido correcto: pide una página, procesa `data`, y si `nextCursor` **no es `null`** vuelve a pedir pasándolo como `cursor`. Repite hasta que sea `null`. :::warning[No infieras el final por una página corta] `count` puede ser **menor** que el `limit` que pediste aunque todavía queden resultados. La única señal de fin es `nextCursor: null`. Un cliente que corta al ver una página corta se pierde datos. ::: :::info[Por qué los fills son la excepción] Un fill es un registro de ejecución: solo se añade, nunca se reordena. Con `OFFSET n` la base de datos vuelve a recorrer y descartar `n` filas en cada página, y —lo que importa más en una API de dinero— **la ventana se mueve bajo tus pies**: un fill nuevo durante el recorrido empuja una fila de la página 2 a la página 3, y un cliente que está paginando su propio historial **se la salta en silencio**. Un cursor _keyset_ es estable frente a inserciones concurrentes y cuesta lo mismo en la página 1 que en la 500. Es también lo que hace el mercado para este recurso concreto: `/fills` de Coinbase pagina por cursor y `myTrades` de Binance avanza por `fromId`. Lo que se cede son los totales, y para un registro que solo crece "cuántos fills he tenido en total" no compensa un `COUNT` completo en cada página. ::: :::caution[El cursor es opaco] `nextCursor` es un token opaco: devuélvelo tal cual. Su codificación no es parte del contrato y puede cambiar. No lo parsees, no lo construyas y no lo guardes como si fuera un identificador estable. ::: ## Filtros En v2, los filtros se expresan como parámetros de _query_ en lugar de rutas especiales. Por ejemplo, para ver los fills de una orden usa `GET /v2/fills?orderId=…` en vez de una ruta dedicada. Consulta la [Referencia de la API](/reference) para ver los filtros disponibles en cada colección. --- ## Límites de tasa Los límites usan un **presupuesto de puntos por ventana de 60 segundos**, según el **nivel** (_tier_) de tu llave. Cada operación consume puntos según su _peso_: una lectura estándar cuesta **1 punto**; las operaciones más pesadas o que hacen _fan-out_ cuestan más. ## Se aplica en dos dimensiones El presupuesto del nivel se comprueba **a la vez** por cuenta y por llave, y gana el más restrictivo: - **Por cuenta** — el límite principal. El presupuesto del nivel se comparte entre **todas** las llaves de tu cuenta, así que crear llaves adicionales **no** multiplica tu límite total. - **Por llave** — cada llave usa el presupuesto del nivel por defecto, y puede configurarse con un **sub-límite** más estricto para acotar una llave concreta. ## Niveles Las llaves nuevas empiezan en **`basic`**. Los niveles son **bandas de límite**, no planes de facturación. | Nivel | Presupuesto | Aprox. | |---|---|---| | `basic` (por defecto) | 300 puntos / min | ~5 req/s | | `standard` | 600 puntos / min | ~10 req/s | | `premium` | 6000 puntos / min | ~100 req/s | :::info[¿Necesitas un límite mayor?] Para subir de nivel, **contacta a soporte de Skipo**. Los niveles los ajusta Skipo; no son autoservicio. ::: ## Coste por herramienta MCP El [servidor MCP](/mcp) consume del **mismo presupuesto** que la API REST: una llamada a una herramienta cuesta los puntos que se indican abajo (no 1 punto fijo). | Puntos | Herramientas | |---|---| | 1 | `check_api_health`, `get_server_time` | | 2 | `get_account`, `get_balances`, `get_contacts`, `get_deposits`, `get_withdrawals`, `get_orders`, `get_assets`, `get_markets` | | 3 | `get_fills`, `get_order_fills`, `get_ledger`, `get_balance_history` | | **10** | `get_indicative_price` | :::caution[`get_indicative_price` cuesta 10 puntos] Es la única herramienta que llega a un **proveedor de precios externo**, y por eso cuesta 10 veces más que una lectura simple. En el nivel `basic` (300 puntos/min) eso son unas **30 llamadas por minuto** antes de que te limiten, aunque el resto de tu presupuesto esté sin usar. Si vas a pedir precios en bucle, considera espaciar las llamadas o pedir un nivel mayor. ::: ## Cabeceras Cada respuesta autenticada incluye las cabeceras estándar (borrador IETF) y sus equivalentes heredadas: | Cabecera | Significado | |---|---| | `RateLimit-Limit` · `X-RateLimit-Limit` | Presupuesto de la ventana actual. | | `RateLimit-Remaining` · `X-RateLimit-Remaining` | Puntos restantes en la ventana. | | `RateLimit-Reset` · `X-RateLimit-Reset` | Segundos hasta que se reinicia la ventana. | ## Respuesta `429` Si superas el límite recibes un `429 Too Many Requests` con cuerpo [`application/problem+json`](/concepts/errors) y una cabecera `Retry-After`: ```http HTTP/1.1 429 Too Many Requests Retry-After: 3 Content-Type: application/problem+json ``` ```json { "type": "https://docs.skipo.com/errors/rate_limited", "title": "Rate limit exceeded", "status": 429, "code": "rate_limited", "retryable": true } ``` **Respeta siempre `Retry-After`** y aplica _backoff_ exponencial ante `429`. --- ## Acciones tokenizadas (xStocks) Las acciones tokenizadas de Skipo (`NVDASPV`, `TSLASPV`, `AMDSPV`, `GLDSPV`…) son activos **rebasing**. La cantidad liquidada nunca se mueve; las acciones corporativas —reinversión de dividendos, _splits_, _splits_ inversos— se expresan subiendo un **multiplicador**: ``` acciones subyacentes = cantidad liquidada × multiplicador precio por acción = tasa / multiplicador ``` Las dos representaciones describen el mismo dinero: `cantidad × tasa` da el mismo importe en la moneda de cotización en ambos casos. El rebase afecta **solo a la pata base** — un mercado xStock es `-CLP` o `-USDT`, así que la otra pata es fiat o una _stablecoin_. ## `assetFormat` Los endpoints de órdenes y fills aceptan un parámetro `assetFormat`: | Valor | Qué devuelve | |---|---| | `rebased` **(por defecto)** | Importes en términos de la **acción subyacente** — lo que un tenedor considera "sus acciones". | | `base` | Importes en términos de los **tokens SPV liquidados** — lo que se movió en cadena y en el libro contable. | ```bash GET /v2/fills?market=NVDASPV-CLP # rebased (por defecto) GET /v2/fills?market=NVDASPV-CLP&assetFormat=base # cifras liquidadas ``` :::info[El valor por defecto es `rebased`] Es un cambio deliberado respecto al comportamiento anterior, que devolvía `base`. Un inversor piensa en acciones, no en tokens SPV, y es lo que hacen las plataformas comparables (el `rebase_multiplier` de Kraken tiene el mismo valor por defecto). Si tu integración depende de las cifras liquidadas, pide `assetFormat=base` explícitamente. ::: En los demás activos no cambia nada: el multiplicador es `1` y ambos modos son idénticos. ## El campo `multiplier` Órdenes y fills publican el factor aplicado: ```json { "id": "1096473", "baseAsset": "NVDASPV", "baseAmount": "10.000658218334353", "rate": "18499.8", "multiplier": "1.0000658218334353" } ``` Con él puedes reconstruir la otra representación sin pedir de nuevo: - **base → rebased:** `baseAmount × multiplier`, `rate ÷ multiplier` - **rebased → base:** `baseAmount ÷ multiplier`, `rate × multiplier` Esto es lo que permite conciliar un importe de la API contra un saldo en cadena. ## El multiplicador es **puntual en el tiempo** Es la parte que más importa y la más fácil de equivocar: > El factor que publicamos es el que estaba vigente **en el momento de ese fill**, leído de > la propia fila. Nunca se recalcula con el factor de hoy. Una acción corporativa cambia el factor **hacia adelante**; no cambia lo que entregó una operación pasada. Aplicar el multiplicador de hoy a un fill del año pasado **reescribe la historia**: si un activo hace un _split_ 10:1, todo tu historial pasado aparecería multiplicado por diez. Es la regla 3 de la guía de la extensión _Scaled UI Amount_ de Solana: un importe histórico debe mostrarse con el multiplicador vigente cuando ocurrió la transacción. :::caution[`multiplier: "1"` no significa "no es una acción tokenizada"] Un factor de exactamente `1` es simplemente un factor que todavía no se ha movido — `TSLASPV` está hoy en `1`. **No lo uses para clasificar activos.** La clase de activo es dato de referencia y se consulta en `GET /v2/assets/{assetSymbol}`. ::: ## El libro contable no acepta `assetFormat` `GET /v2/ledger` expone deliberadamente los importes **liquidados**, sin `assetFormat`. El libro es el registro de doble entrada de lo que realmente se movió, y hoy sus asientos no llevan el multiplicador vigente en el momento del apunte. Honrar `assetFormat` obligaría a resolver el factor **en vivo**, que es exactamente el error de reescritura descrito arriba. Preferimos una cifra correcta y sin escalar a una cifra escalada y potencialmente falsa. Para ver un fill en términos de acciones, úsalo desde `/v2/fills`, que sí lleva el factor de su momento. --- ## Webhooks(Concepts) En lugar de hacer _polling_, puedes registrar una URL para recibir **webhooks** cuando cambian tus transacciones. Cada entrega va **firmada** para que puedas verificar que proviene de Skipo y que no fue alterada. ## Envoltura del evento Cada webhook tiene una envoltura estable inspirada en el modelo de Fireblocks v2: ```json { "id": "evt_01H…", "webhookId": "whk_01H…", "eventType": "fill.created", "resourceId": "1096473", "createdAt": 1783003522123, "data": { "id": "1096473", "orderId": "clord_01H…", "side": "BUY" } } ``` | Campo | Tipo | Descripción | |---|---|---| | `id` | string | Id **lógico** del evento. Es tu clave de idempotencia. | | `webhookId` | string | El endpoint que recibió la entrega. | | `eventType` | string | Uno del [catálogo](#catálogo-de-eventos). | | `resourceId` | string \| null | Id del recurso afectado. | | `createdAt` | **number** | Instante del evento, en **milisegundos desde epoch**. | | `data` | object | Snapshot del recurso afectado. Sus nombres de campo **no** son necesariamente idénticos a los de la representación REST del mismo recurso. | :::warning[`createdAt` es un número, no una cadena ISO-8601] La envoltura usa **epoch en milisegundos** (`1783003522123`), no `"2026-07-10T12:34:56.000Z"`. Versiones anteriores de esta página mostraban ISO-8601: **la documentación estaba equivocada, no el _payload_**. Si tu integración parsea ese campo como cadena, corrígela. La asimetría es **deliberada**: la envoltura la produce la plataforma de webhooks y usa epoch (un entero, sin ambigüedad de zona horaria ni de formato); las marcas de tiempo **dentro de `data`** (`createdAt`, `executedAt`) son ISO-8601, porque son exactamente los mismos valores que devuelve la API REST. Regla práctica: **fuera de `data`, epoch; dentro de `data`, ISO-8601.** ::: :::warning[`data` no es la respuesta REST] `data` es un snapshot del recurso afectado, pero **no** garantizamos que sus campos se llamen igual que en la representación REST de ese mismo recurso. Divergencias conocidas hoy: - el _payload_ del webhook emite `asset` donde el recurso REST publica `assetSymbol`; - emite un `assetId` compuesto (por ejemplo `"USDT-TRON"`) donde el recurso REST publica un `networkSymbol` suelto. Además, el _payload_ de un fill no trae todos los campos que sí lleva el recurso REST del fill. Escribe tu integración contra el _payload_ del webhook: no reutilices el _parser_ de tus respuestas REST. ::: ### Idempotencia y reenvíos `id` es el id **lógico** del evento y es **estable entre reenvíos**: si Skipo reintenta la entrega, o el evento se reenvía, llega el **mismo** `id`. Deduplica con él y un webhook de movimiento de dinero nunca se procesará dos veces. Cada entrega llega además con estas cabeceras: | Cabecera | Contenido | |---|---| | `skipo-webhook-delivery-id` | Id de **esta entrega**. Es el mismo en todos los reintentos automáticos de una entrega; solo cambia en un reenvío. | | `skipo-webhook-event` | El `eventType`, para enrutar sin parsear el cuerpo. | | `skipo-webhook-signature` | La firma JWS _detached_ (ver [abajo](#verificación-de-la-firma)). | ## Catálogo de eventos Son **7 eventos**. Los tipos usan notación de puntos y admiten comodines al suscribirte: exacto (`withdrawal.created`), por categoría (`withdrawal.*`) o global (`*`). | Evento | Cuándo | |---|---| | `withdrawal.created` | Se crea un retiro y se congela el saldo. | | `withdrawal.status.updated` | Un retiro cambia de estado (incluido el terminal COMPLETED / FAILED). | | `deposit.created` | Se detecta y registra un depósito. | | `deposit.status.updated` | Un depósito cambia de estado (incluido REVERSED). | | `order.created` | Se coloca una orden de conversión (al confirmar), con `status: "NEW"`. | | `order.status.updated` | Una orden cambia de estado (`PARTIALLY_FILLED` → `FILLED` / `FAILED`). | | `fill.created` | Se ejecuta un _fill_ contra una orden. | Las categorías suscribibles son exactamente `withdrawal.*`, `deposit.*`, `order.*` y `fill.*`. :::info[Órdenes y fills] Una **orden** (una por confirmación) se completa con **1..N fills**. Una conversión normal tiene exactamente un fill; una orden a crédito (_capacity_) se va completando con varios a medida que se paga. Por eso `fill.created` puede llegar varias veces para un mismo `orderId` — consulta [Ids y correlación](/concepts/ids-and-correlation). ::: :::note[`webhook.test` no es suscribible] Un _ping_ de prueba llega con `test: true` para que verifiques el cableado de tu endpoint. **No** es un evento del catálogo y no puedes suscribirte a él: una prueba es algo que se dispara, no algo que ocurre en tu cuenta. Todavía no hay endpoint público para dispararla — v2 no expone gestión de webhooks, solo el JWKS de verificación en `/.well-known`. ::: :::caution[Ignora los tipos que no conozcas] Pueden aparecer eventos nuevos en cualquier momento. Un consumidor correcto **descarta en silencio** un `eventType` que no reconoce, en lugar de fallar. Suscribirse con `*` implica aceptar ese contrato. ::: ### Política de cambios Pueden aparecer campos nuevos en `data` en cualquier momento. Trátalo como un objeto extensible: parsea los campos que te importan e ignora el resto. ## Los suppliers también son consumidores Un **supplier** (la contraparte que ejecuta una conversión) es un consumidor de la API **de primera clase**, simétrico a cualquier otro: usa las mismas llaves, los mismos endpoints REST y los mismos webhooks, para su propia conciliación y automatización. Lo que ve es **su lado** de la operación: - sus propios retiros y depósitos; - las órdenes y fills en los que **él fue el supplier seleccionado**. Una conversión es bilateral: una misma ejecución produce un evento para el cliente y otro para el supplier, cada uno con **su** perspectiva (el `side` está invertido). No es una capacidad interna ni oculta — es el mismo contrato documentado en esta página. ## Verificación de la firma Cada entrega incluye una firma **JWS detached** (EdDSA/Ed25519) en la cabecera `skipo-webhook-signature`, con una marca de tiempo firmada en el _header_ crítico `skipo.io/iat`. Verifícala con la clave pública publicada en el **JWKS**: ``` GET https://api.skipo.com/v2/.well-known/webhook-jwks.json ``` El valor de la cabecera es un JWS en forma **_detached_**: el segmento del _payload_ va vacío, así que lleva dos puntos seguidos sin nada entre ellos: ``` eyJhbGciOiJFZERTQSIsImtpZCI6IndoaS….. ``` El _header_ protegido lleva el id de la clave y la marca de tiempo firmada: ```json { "alg": "EdDSA", "kid": "whk_939492a5e222", "crit": ["skipo.io/iat"], "skipo.io/iat": 1786030000 } ``` Pasos de verificación: 1. Parte la cabecera por `.` en ``, un segmento vacío y ``. 2. Busca el JWK cuyo `kid` coincida con el del _header_ protegido (cachea el JWKS). 3. Reconstruye la entrada de firma **reinsertando el _payload_**: ` + "." + base64url(cuerpoCrudo)`. 4. Verifica la firma Ed25519 sobre esa entrada. 5. Compara `skipo.io/iat` con tu reloj y rechaza cualquier cosa desviada más de **300 segundos** (5 minutos) en cualquier dirección. Esto es lo que impide reproducir (_replay_) una entrega capturada. :::warning Verifica siempre contra los **bytes crudos** del cuerpo, antes de parsear el JSON. Reserializar el JSON cambia los bytes y la firma dejará de validar, aunque el objeto parseado sea idéntico. ::: ### Node.js Sin dependencias: `node:crypto` verifica Ed25519 y acepta el JWK directamente. ```js import crypto from 'node:crypto' const MAX_SKEW_SECONDS = 300 function verifySkipoWebhook(rawBody, signatureHeader, jwks) { const [protectedB64, empty, signatureB64] = signatureHeader.split('.') if (empty !== '' || !protectedB64 || !signatureB64) throw new Error('malformed signature header') const header = JSON.parse(Buffer.from(protectedB64, 'base64url').toString('utf8')) if (header.alg !== 'EdDSA') throw new Error(`unexpected alg ${header.alg}`) const jwk = jwks.keys.find((k) => k.kid === header.kid) if (!jwk) throw new Error(`unknown kid ${header.kid}`) // JWS detached: reinserta el payload en base64url para reconstruir la entrada de firma. const signingInput = `${protectedB64}.${Buffer.from(rawBody).toString('base64url')}` const key = crypto.createPublicKey({ key: jwk, format: 'jwk' }) const ok = crypto.verify(null, Buffer.from(signingInput), key, Buffer.from(signatureB64, 'base64url')) if (!ok) throw new Error('bad signature') const iat = header['skipo.io/iat'] if (typeof iat !== 'number') throw new Error('missing skipo.io/iat') if (Math.abs(Math.floor(Date.now() / 1000) - iat) > MAX_SKEW_SECONDS) { throw new Error('stale signature (replay?)') } return JSON.parse(Buffer.from(rawBody).toString('utf8')) } ``` En Express, obtén los bytes crudos con `express.raw({ type: 'application/json' })`: `express.json()` los parsea y los descarta, y entonces ya no se puede comprobar la firma. ### Python ```python import base64, json, time from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from cryptography.exceptions import InvalidSignature MAX_SKEW_SECONDS = 300 def _b64u_decode(s: str) -> bytes: return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)) def _b64u_encode(b: bytes) -> str: return base64.urlsafe_b64encode(b).rstrip(b"=").decode() def verify_skipo_webhook(raw_body: bytes, signature_header: str, jwks: dict) -> dict: parts = signature_header.split(".") if len(parts) != 3 or parts[1] != "": raise ValueError("malformed signature header") protected_b64, _, signature_b64 = parts header = json.loads(_b64u_decode(protected_b64)) if header.get("alg") != "EdDSA": raise ValueError(f"unexpected alg {header.get('alg')}") jwk = next((k for k in jwks["keys"] if k.get("kid") == header.get("kid")), None) if jwk is None: raise ValueError(f"unknown kid {header.get('kid')}") # JWS detached: reinserta el payload en base64url para reconstruir la entrada de firma. signing_input = f"{protected_b64}.{_b64u_encode(raw_body)}".encode() key = Ed25519PublicKey.from_public_bytes(_b64u_decode(jwk["x"])) try: key.verify(_b64u_decode(signature_b64), signing_input) except InvalidSignature: raise ValueError("bad signature") iat = header.get("skipo.io/iat") if not isinstance(iat, int): raise ValueError("missing skipo.io/iat") if abs(int(time.time()) - iat) > MAX_SKEW_SECONDS: raise ValueError("stale signature (replay?)") return json.loads(raw_body) ``` ### Rotación de claves El `kid` existe para poder rotar la clave de firma sin romper tu integración. Selecciona el JWK por el `kid` del _header_ protegido en vez de asumir una sola clave, cachea el JWKS y vuelve a pedirlo cuando veas un `kid` desconocido. No fijes el material de la clave. ## Reintentos Si tu endpoint no responde `2xx`, Skipo reintenta con un calendario de _backoff_. Si un endpoint falla de forma sostenida, se **suspende** automáticamente y puedes reactivarlo desde el panel. Las entregas del mismo recurso se serializan en la medida de lo posible, pero **el orden no está garantizado**: usa `createdAt` y el `status` del recurso para decidir cuál es el estado más reciente, en vez de asumir el orden de llegada. ### Reenvíos Un evento reenviado llega con el **mismo** `id` lógico, así que si deduplicas por él (como debes) reenviar algo que ya procesaste no tiene efecto. Llega con un `skipo-webhook-delivery-id` **nuevo**, y así distingues un reenvío del original. :::info[La gestión de endpoints vive en el panel] Registrar, editar o suspender un endpoint de webhook se hace desde el panel de Skipo, no desde la API autenticada por llave. Es deliberado: una llave filtrada no debe poder redirigir tus notificaciones. ::: --- ## api_access_disabled {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `api_access_disabled` El acceso a la API de esta cuenta fue deshabilitado. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 403 | `api_access_disabled` | no | ## Cuándo ocurre Un operador de Skipo suspendió el acceso de la cuenta a la API pública. Afecta a todas las llaves y a todos los métodos de autenticación, no a una llave en particular. ## Cómo resolverlo Contacta a soporte. La credencial es válida: volver a autenticarte o crear una llave nueva no restablece el acceso, solo un operador puede reactivarlo. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/api_access_disabled", "title": "API access disabled", "status": 403, "code": "api_access_disabled", "retryable": false, "detail": "El acceso a la API de esta cuenta fue deshabilitado." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## clock_skew {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `clock_skew` El reloj del cliente está demasiado adelantado respecto al servidor. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 401 | `clock_skew` | sí | ## Cuándo ocurre El claim `iat` supera la hora del servidor por más de la tolerancia permitida. ## Cómo resolverlo Sincroniza tu reloj con `GET /v2/time` y vuelve a firmar. Es reintentable una vez corregido el desfase. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/clock_skew", "title": "Clock skew too large", "status": 401, "code": "clock_skew", "retryable": true, "detail": "El reloj del cliente está demasiado adelantado respecto al servidor." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## conflict {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `conflict` La petición choca con el estado actual del recurso. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 409 | `conflict` | no | ## Cuándo ocurre Un cambio que contradice el estado presente (por ejemplo un nombre duplicado). ## Cómo resolverlo Vuelve a leer el recurso, ajusta la petición al estado actual y reintenta. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/conflict", "title": "Conflict", "status": 409, "code": "conflict", "retryable": false, "detail": "La petición choca con el estado actual del recurso." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## downstream_unavailable {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `downstream_unavailable` Un servicio del que depende la operación no está disponible. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 502 | `downstream_unavailable` | sí | ## Cuándo ocurre Un componente interno o proveedor está caído o no responde. ## Cómo resolverlo Reintenta con backoff. Suele resolverse solo en poco tiempo. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/downstream_unavailable", "title": "Downstream service unavailable", "status": 502, "code": "downstream_unavailable", "retryable": true, "detail": "Un servicio del que depende la operación no está disponible." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## idempotency_conflict {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `idempotency_conflict` Se reutilizó una clave de idempotencia con un cuerpo distinto. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 409 | `idempotency_conflict` | no | ## Cuándo ocurre Enviaste la misma clave de idempotencia con parámetros diferentes a la petición original. ## Cómo resolverlo Usa una clave de idempotencia nueva para una petición distinta, o reenvía exactamente el mismo cuerpo. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/idempotency_conflict", "title": "Idempotency conflict", "status": 409, "code": "idempotency_conflict", "retryable": false, "detail": "Se reutilizó una clave de idempotencia con un cuerpo distinto." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## Catálogo de errores {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # Catálogo de errores Toda respuesta de error usa el formato [`application/problem+json` (RFC 9457)](/concepts/errors). El campo `type` apunta a la página de este catálogo correspondiente a su `code`: `https://docs.skipo.com/errors/{code}`. El `code` es un **contrato estable** — ramifica tu lógica sobre él. | `code` | HTTP | ¿Reintentable? | Título | |---|---|---|---| | [`validation_error`](/errors/validation_error) | 400 | no | Validation error | | [`unauthorized`](/errors/unauthorized) | 401 | no | Unauthorized | | [`invalid_signature`](/errors/invalid_signature) | 401 | no | Invalid signature | | [`clock_skew`](/errors/clock_skew) | 401 | sí | Clock skew too large | | [`nonce_reused`](/errors/nonce_reused) | 401 | sí | Nonce already used | | [`key_expired`](/errors/key_expired) | 401 | no | API key expired | | [`key_revoked`](/errors/key_revoked) | 401 | no | API key revoked | | [`insufficient_scope`](/errors/insufficient_scope) | 403 | no | Insufficient scope | | [`ip_not_allowed`](/errors/ip_not_allowed) | 403 | no | IP address not allowed | | [`two_factor_required`](/errors/two_factor_required) | 403 | no | Two-factor required | | [`permission_denied`](/errors/permission_denied) | 403 | no | Permission denied | | [`api_access_disabled`](/errors/api_access_disabled) | 403 | no | API access disabled | | [`not_found`](/errors/not_found) | 404 | no | Resource not found | | [`conflict`](/errors/conflict) | 409 | no | Conflict | | [`idempotency_conflict`](/errors/idempotency_conflict) | 409 | no | Idempotency conflict | | [`key_limit_reached`](/errors/key_limit_reached) | 409 | no | API key limit reached | | [`quotation_expired`](/errors/quotation_expired) | 410 | sí | Quotation expired | | [`unprocessable`](/errors/unprocessable) | 422 | no | Unprocessable request | | [`rate_limited`](/errors/rate_limited) | 429 | sí | Rate limit exceeded | | [`internal_error`](/errors/internal_error) | 500 | sí | Internal server error | | [`downstream_unavailable`](/errors/downstream_unavailable) | 502 | sí | Downstream service unavailable | | [`maintenance`](/errors/maintenance) | 503 | sí | Service under maintenance | | [`service_degraded`](/errors/service_degraded) | 503 | sí | Service degraded | --- ## insufficient_scope {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `insufficient_scope` La llave no tiene el scope requerido para la operación. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 403 | `insufficient_scope` | no | ## Cuándo ocurre Llamaste a un endpoint que exige un scope que la llave no incluye (ver `requiredScopes` en la respuesta). ## Cómo resolverlo Crea una llave con los scopes necesarios. Los scopes se fijan al crear la llave y no se amplían después. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/insufficient_scope", "title": "Insufficient scope", "status": 403, "code": "insufficient_scope", "retryable": false, "detail": "La llave no tiene el scope requerido para la operación." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## internal_error {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `internal_error` Ocurrió un error inesperado en Skipo. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 500 | `internal_error` | sí | ## Cuándo ocurre Fallo no controlado del lado del servidor. ## Cómo resolverlo Reintenta con backoff. Si persiste, contacta a Skipo con el `traceId` que viene en el cuerpo de esta respuesta. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/internal_error", "title": "Internal server error", "status": 500, "code": "internal_error", "retryable": true, "detail": "Ocurrió un error inesperado en Skipo." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## invalid_signature {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `invalid_signature` El JWT firmado no verifica contra la clave pública registrada, o alguno de sus claims no coincide con la petición. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 401 | `invalid_signature` | no | ## Cuándo ocurre Firma incorrecta, `sub`/`uri`/`bodyHash` que no coinciden, `exp - iat > 60s`, o falta el `nonce`. ## Valores de `reason` La respuesta trae un campo `reason` que nombra **la primera comprobación que falló**. Las comprobaciones corren en el orden de la tabla, así que un `bad_body_hash` implica que `sub` y `uri` ya coincidían. Ramifica sobre `reason`, no sobre `detail`. | `reason` | Significado | |---|---| | `no_signing_key` | La llave no tiene ninguna clave de firma activa. Sube la clave pública (SPKI) desde el panel. | | `bad_jwt` | El JWT no verifica contra ninguna clave de firma activa, o está malformado o vencido. Verificamos contra **todas** las claves activas, así que esto no es un solapamiento de rotación. | | `bad_sub` | El claim `sub` no es idéntico a la cabecera `X-API-Key`. Ambos son el **prefijo** de la llave. | | `bad_uri_claim` | El claim `uri` no es `"MÉTODO /ruta?query"` exactamente como se envía, query incluida. | | `missing_iat_exp` | Falta `iat` o `exp`, o alguno no es numérico. | | `exp_too_far` | `exp - iat > 60` segundos. | | `bad_body_hash` | `bodyHash` no es el SHA-256 hex de los bytes crudos que enviaste. Serializa el cuerpo una sola vez y envía **esos** bytes. | | `missing_nonce` | Falta el claim `nonce`. Un `nonce` repetido es distinto: devuelve [`nonce_reused`](/errors/nonce_reused). | ## Cómo resolverlo Reconstruye el JWT: `sub` = prefijo de la llave, `uri` = `"MÉTODO /ruta?query"` exacto, `bodyHash` = SHA-256 hex de los bytes crudos del cuerpo, `exp ≤ iat + 60`. Firma con la clave privada cuyo par público subiste. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/invalid_signature", "title": "Invalid signature", "status": 401, "code": "invalid_signature", "retryable": false, "detail": "El JWT firmado no verifica contra la clave pública registrada, o alguno de sus claims no coincide con la petición.", "reason": "bad_sub" } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## ip_not_allowed {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `ip_not_allowed` La IP de origen no está en la lista de IPs permitidas de la llave. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 403 | `ip_not_allowed` | no | ## Cuándo ocurre La llave tiene una lista de IPs permitidas y la petición llegó desde otra dirección. ## Cómo resolverlo Añade la IP saliente de tu servidor a la lista permitida de la llave, o llama desde una IP autorizada. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/ip_not_allowed", "title": "IP address not allowed", "status": 403, "code": "ip_not_allowed", "retryable": false, "detail": "La IP de origen no está en la lista de IPs permitidas de la llave." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## key_expired {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `key_expired` La llave de API alcanzó su fecha de expiración. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 401 | `key_expired` | no | ## Cuándo ocurre La llave tenía una expiración configurada y ya pasó. ## Cómo resolverlo Crea una nueva llave desde el panel y actualiza tu integración. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/key_expired", "title": "API key expired", "status": 401, "code": "key_expired", "retryable": false, "detail": "La llave de API alcanzó su fecha de expiración." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## key_limit_reached {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `key_limit_reached` La cuenta alcanzó su número máximo de llaves de API. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 409 | `key_limit_reached` | no | ## Cuándo ocurre Intentaste crear una llave superando el tope de la cuenta. ## Cómo resolverlo Revoca una llave que ya no uses antes de crear otra, o contacta a Skipo para ampliar el tope. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/key_limit_reached", "title": "API key limit reached", "status": 409, "code": "key_limit_reached", "retryable": false, "detail": "La cuenta alcanzó su número máximo de llaves de API." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## key_revoked {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `key_revoked` La llave de API fue revocada. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 401 | `key_revoked` | no | ## Cuándo ocurre Alguien revocó la llave desde el panel (o fue revocada por seguridad). ## Cómo resolverlo Crea una nueva llave y reemplázala en tu integración. Si no esperabas la revocación, contacta a Skipo. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/key_revoked", "title": "API key revoked", "status": 401, "code": "key_revoked", "retryable": false, "detail": "La llave de API fue revocada." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## maintenance {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `maintenance` La API está en mantenimiento. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 503 | `maintenance` | sí | ## Cuándo ocurre Ventana de mantenimiento planificada; casi todas las rutas responden 503 excepto `/health` y `/v2/time`. ## Cómo resolverlo Reintenta más tarde respetando `Retry-After`. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/maintenance", "title": "Service under maintenance", "status": 503, "code": "maintenance", "retryable": true, "detail": "La API está en mantenimiento." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## nonce_reused {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `nonce_reused` El `nonce` del JWT ya fue utilizado (protección anti-replay). | HTTP | `code` | ¿Reintentable? | |---|---|---| | 401 | `nonce_reused` | sí | ## Cuándo ocurre Reenviaste un JWT ya visto, o generaste el mismo `nonce` dos veces dentro de la ventana de retención. ## Cómo resolverlo Genera un `nonce` único por petición (por ejemplo un UUID v4) y vuelve a firmar. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/nonce_reused", "title": "Nonce already used", "status": 401, "code": "nonce_reused", "retryable": true, "detail": "El `nonce` del JWT ya fue utilizado (protección anti-replay)." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## not_found {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `not_found` El recurso solicitado no existe o no pertenece a tu cuenta. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 404 | `not_found` | no | ## Cuándo ocurre Un identificador inexistente, o un recurso de otra cuenta (Skipo no revela su existencia). ## Cómo resolverlo Verifica el identificador. Recuerda que solo puedes acceder a tus propios recursos. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/not_found", "title": "Resource not found", "status": 404, "code": "not_found", "retryable": false, "detail": "El recurso solicitado no existe o no pertenece a tu cuenta." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## permission_denied {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `permission_denied` La cuenta no está autorizada para esta operación. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 403 | `permission_denied` | no | ## Cuándo ocurre Restricciones a nivel de cuenta impiden la acción, independientemente del scope de la llave. ## Cómo resolverlo Contacta a Skipo si crees que tu cuenta debería tener acceso. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/permission_denied", "title": "Permission denied", "status": 403, "code": "permission_denied", "retryable": false, "detail": "La cuenta no está autorizada para esta operación." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## quotation_expired {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `quotation_expired` La cotización que intentas confirmar ya expiró. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 410 | `quotation_expired` | sí | ## Cuándo ocurre Confirmaste una conversión demasiado tarde; las cotizaciones tienen una vida corta. ## Cómo resolverlo Pide una cotización nueva (`POST /v2/quotes`) y confírmala de inmediato. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/quotation_expired", "title": "Quotation expired", "status": 410, "code": "quotation_expired", "retryable": true, "detail": "La cotización que intentas confirmar ya expiró." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## rate_limited {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `rate_limited` Superaste el límite de tasa o la cuota de la llave/cuenta. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 429 | `rate_limited` | sí | ## Cuándo ocurre Demasiadas peticiones en la ventana, o se agotó la cuota diaria. ## Cómo resolverlo Respeta la cabecera `Retry-After` y aplica backoff exponencial. Consulta la guía de Límites de tasa. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/rate_limited", "title": "Rate limit exceeded", "status": 429, "code": "rate_limited", "retryable": true, "detail": "Superaste el límite de tasa o la cuota de la llave/cuenta." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## service_degraded {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `service_degraded` El servicio opera de forma degradada y rechazó temporalmente la petición. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 503 | `service_degraded` | sí | ## Cuándo ocurre Protección de carga o una dependencia degradada. ## Cómo resolverlo Reintenta con backoff respetando `Retry-After`. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/service_degraded", "title": "Service degraded", "status": 503, "code": "service_degraded", "retryable": true, "detail": "El servicio opera de forma degradada y rechazó temporalmente la petición." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## two_factor_required {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `two_factor_required` La operación requiere verificación de segundo factor (2FA). | HTTP | `code` | ¿Reintentable? | |---|---|---| | 403 | `two_factor_required` | no | ## Cuándo ocurre Operaciones sensibles de gestión de cuenta que exigen 2FA. ## Cómo resolverlo Completa el paso 2FA desde el panel. La gestión sensible (crear llaves, cambiar destinos) es solo por el panel, no por la API. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/two_factor_required", "title": "Two-factor required", "status": 403, "code": "two_factor_required", "retryable": false, "detail": "La operación requiere verificación de segundo factor (2FA)." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## unauthorized {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `unauthorized` Falta la credencial o no es válida. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 401 | `unauthorized` | no | ## Cuándo ocurre No enviaste `Authorization: Bearer`, la llave es desconocida, o una operación firmada llegó sin `X-API-Key` + JWT. ## Valores de `reason` En una operación firmada, `X-API-Key` toma el **prefijo** de la llave, no el secreto completo. Si envías el secreto completo, no resuelve ninguna llave y recibes este error con `detail: "Unknown API key."` — no `invalid_signature`. | `reason` | Significado | |---|---| | `signed_jwt_on_bearer_route` | Firmaste un endpoint de Nivel 1. Tu llave está bien: sobra la firma. Reenvía con `Authorization: Bearer ` y sin `X-API-Key`. | ## Cómo resolverlo Verifica que envías la llave correcta para el entorno (`skp_live_` vs `skp_test_`). Para operaciones que mueven dinero, usa el flujo firmado (nivel 2). ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/unauthorized", "title": "Unauthorized", "status": 401, "code": "unauthorized", "retryable": false, "detail": "Falta la credencial o no es válida." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## unprocessable {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `unprocessable` La petición es sintácticamente válida pero no puede procesarse por una regla de negocio. | HTTP | `code` | ¿Reintentable? | |---|---|---| | 422 | `unprocessable` | no | ## Cuándo ocurre Por ejemplo, saldo insuficiente o un destino no permitido. ## Cómo resolverlo Revisa `detail` para la regla concreta que falló y ajusta la operación. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/unprocessable", "title": "Unprocessable request", "status": 422, "code": "unprocessable", "retryable": false, "detail": "La petición es sintácticamente válida pero no puede procesarse por una regla de negocio." } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## validation_error {/* AUTO-GENERATED by scripts/gen-errors.mjs — do not edit; run `yarn gen:errors`. */} # `validation_error` La petición no cumple el esquema esperado (campo faltante, tipo incorrecto o valor inválido). | HTTP | `code` | ¿Reintentable? | |---|---|---| | 400 | `validation_error` | no | ## Cuándo ocurre El cuerpo, los parámetros de query o las cabeceras no pasan la validación de entrada. Incluye un caso que sorprende a muchos clientes: **un parámetro que no reconocemos se rechaza, no se ignora**. Si mandas un nombre con una errata o uno que ya retiramos, recibes un 400 en vez de una respuesta silenciosamente sin filtrar — deliberado en una API de dinero, donde un filtro ignorado devuelve el resultado equivocado sin avisar. ## Cómo resolverlo Lee `detail` (la lista completa de fallos, separada por `; `) y el array `errors`, que trae los mismos mensajes uno por uno para procesarlos por código. Nombran el campo exacto: `property asset should not exist` significa que ese parámetro no existe — revisa el nombre en la referencia del endpoint. No reintentes sin cambiar la petición. ## Ejemplo ```json { "type": "https://docs.skipo.com/errors/validation_error", "title": "Validation error", "status": 400, "code": "validation_error", "retryable": false, "detail": "property asset should not exist; assetSymbol must be a valid asset symbol" } ``` --- Volver al [catálogo de errores](/errors) · Ver el [formato de errores](/concepts/errors). --- ## Entornos Skipo expone **un solo entorno**: producción. | Entorno | Base URL | |---|---| | Producción | `https://api.skipo.com/v2` | :::warning No existe un entorno de pruebas. Con cualquier llave, sea cual sea su prefijo, lees saldos reales, cotizas con precios reales y ejecutas retiros y conversiones reales. El prefijo (`skp_live_…` o `skp_test_…`) es solo un formato de llave: la API no lo interpreta y no cambia el comportamiento de ninguna operación. ::: ## Cabeceras estándar La API **no** devuelve una cabecera `X-Request-Id`. La traza para soporte y depuración viaja en el cuerpo de las respuestas de error, pero la aportas tú: - Envía tu propia cabecera `X-Request-Id` en cada petición. Skipo la reutiliza como `traceId` de esa petición, así puedes correlacionarla con tus propios registros. - `traceId` — campo del cuerpo de error (`application/problem+json`). Solo aparece si enviaste esa cabecera; cítalo al reportar incidencias. ## Zona horaria y relojes Las operaciones firmadas (nivel 2) son sensibles al desfase de reloj. Consulta la hora del servidor con `GET /v2/time` antes de firmar si sospechas de un desfase. --- ## Quickstart En esta guía harás tu primera llamada autenticada contra la API de Skipo usando una llave _bearer_ de solo lectura. ## 1. Obtén una llave de API Las llaves se generan desde el panel de Skipo (auto-servicio, con verificación 2FA). Cada llave nace con un conjunto de **scopes** y se muestra **una sola vez**. Guarda la llave en un lugar seguro; Skipo solo almacena su hash. :::warning[No existe un entorno de pruebas] Cualquier llave opera sobre saldos y precios reales: un retiro o una orden se ejecutan de verdad. Para tu primera llamada usa una operación de solo lectura como `GET /v2/balances`. ::: ## 2. Haz una llamada de lectura Las lecturas y escrituras no sensibles se autentican con la llave en la cabecera `Authorization: Bearer`: ```bash curl https://api.skipo.com/v2/balances \ -H "Authorization: Bearer skp_live_your_key_here" ``` Respuesta (abreviada): ```json [ { "assetSymbol": "USDC", "balance": "1250.00", "balanceFrozen": "0", "balancePending": "0", "balancePrefCurrency": "1187500", "balanceUSD": "1250.00" } ] ``` ## 3. Explora la referencia Todas las operaciones disponibles están en la **[Referencia de la API](/reference)**, con ejemplos de código en varios lenguajes generados desde la especificación OpenAPI. La especificación siempre vigente la sirve la propia API en [`https://api.skipo.com/v2/openapi.json`](https://api.skipo.com/v2/openapi.json). ## Siguientes pasos - **[Autenticación](/concepts/authentication)** — cómo firmar operaciones que mueven dinero (retiros, conversiones). - **[Webhooks](/concepts/webhooks)** — recibe notificaciones cuando cambian tus transacciones en lugar de hacer _polling_. - **[Límites de tasa](/concepts/rate-limits)** — cómo leer las cabeceras `RateLimit-*` y evitar el `429`. --- ## Skipo API Bienvenido a la documentación de la **API pública de Skipo** (`v2`). Con la API puedes consultar balances y movimientos, obtener datos de mercado, crear retiros y conversiones, y recibir **webhooks** firmados cuando cambian tus transacciones. :::info[Versión] Esta documentación describe la versión **v2**, servida bajo `https://api.skipo.com/v2`. La versión v1 sigue disponible hasta el **10 de septiembre de 2026**, fecha en la que se retira — consulta [Migración desde v1](/migration-from-v1). ::: ## Empieza aquí - **[Primeros pasos → Quickstart](/getting-started/quickstart)** — tu primera llamada autenticada en menos de cinco minutos. - **[Autenticación](/concepts/authentication)** — el esquema de dos niveles: llaves _bearer_ para lecturas y JWT firmado por petición para mover dinero. - **[Referencia de la API](/reference)** — todas las operaciones, esquemas y ejemplos, renderizados desde la especificación OpenAPI. La versión siempre al día la sirve la propia API en [`api.skipo.com/v2/openapi.json`](https://api.skipo.com/v2/openapi.json). - **[Referencia de endpoints](/api)** — la misma superficie en texto, página por sección, pensada para leer y para buscar. ## Para agentes y herramientas de IA La documentación se publica también en formato apto para modelos de lenguaje: - [`/llms.txt`](pathname:///llms.txt) — índice curado de la documentación. - [`/llms-full.txt`](pathname:///llms-full.txt) — todo el contenido en un solo archivo. - [`/openapi.json`](pathname:///openapi.json) — la especificación OpenAPI cruda. ¿Prefieres conectar tu cuenta directamente a un agente? Usa el [servidor MCP de solo lectura](/mcp). ## Conceptos clave | Concepto | Descripción | |---|---| | [Autenticación](/concepts/authentication) | Llaves `skp_live_…` / `skp_test_…` y firma EdDSA para operaciones sensibles. | | [Límites de tasa](/concepts/rate-limits) | Límites por llave, cabeceras `RateLimit-*` y cuotas mensuales. | | [Errores](/concepts/errors) | Respuestas `application/problem+json` (RFC 9457) con `code` estable. | | [Paginación](/concepts/pagination) | Envoltura consistente: offset por defecto, cursor en fills. | | [Ids y correlación](/concepts/ids-and-correlation) | Cómo se enlazan retiros, depósitos, órdenes, fills y el libro contable. | | [Webhooks](/concepts/webhooks) | Los 7 eventos, la envoltura y la firma JWS (EdDSA) verificable vía JWKS. | | [Acciones tokenizadas](/concepts/tokenized-equities) | `assetFormat`, el multiplicador y por qué es puntual en el tiempo. | --- ## Servidor MCP El **servidor MCP de Skipo** expone la API pública v2 a través del [Model Context Protocol](https://modelcontextprotocol.io), para que puedas conectar tu cuenta a **Claude** (Desktop, Code o cualquier cliente compatible) y preguntar por tus balances, transacciones y actividad de cambio en lenguaje natural. El agente traduce tus preguntas a llamadas de lectura contra la API. :::info[Solo lectura] El servidor MCP de Skipo **solo emite peticiones `GET`**. No puede mover dinero, crear retiros ni ejecutar conversiones — aunque se lo pidas. No existe ninguna herramienta de escritura. ::: ## Cómo conectar Skipo aloja el servidor; no hay nada que instalar y no manejas llaves de API. - **[Conector remoto (OAuth)](/mcp/remote-connector)** — añade `https://api.skipo.com/mcp` como conector personalizado en tu cliente, inicia sesión con tu cuenta Skipo (OAuth 2.1) y aprueba el acceso de solo lectura. Puedes revocarlo cuando quieras desde el panel, en **Aplicaciones conectadas**. ## Herramientas Son **15 herramientas**, todas de solo lectura, y se corresponden con operaciones `GET` de la [API v2](/reference). Los importes se devuelven como **cadenas decimales** (conserva la precisión) y las fechas en **ISO-8601**. Las colecciones paginadas llegan como `{ items, pagination }`. Ojo: **no es la misma envoltura que la API REST**, que usa [`{ data, pagination }`](/concepts/pagination) — el servidor MCP renombra `data` a `items` para que todas las herramientas devuelvan la misma forma. El bloque `pagination` es idéntico. `get_balances` es la excepción: devuelve la lista completa como arreglo, sin envoltura ni paginación. ### Cuenta y balances Requiere el scope `accounts:read`. | Herramienta | Devuelve | |---|---| | `get_account` | Perfil de la cuenta: nombre, identificación, estado de verificación, moneda preferida. | | `get_balances` | Balances por moneda (disponible, congelado, pendiente, valor en USD). | | `get_balance_history` | Instantáneas históricas de balance de una moneda (por día/semana/mes). | | `get_ledger` | Asientos del libro contable de **una** moneda, cada uno con su `source` — la superficie de conciliación. | ### Transferencias Requiere el scope `transfers:read`. | Herramienta | Devuelve | |---|---| | `get_withdrawals` | Historial de retiros con filtros; o uno por `transaction_id`. | | `get_deposits` | Historial de depósitos con filtros; o uno por `transaction_id`. | ### Convert (órdenes y fills) Requiere el scope `trading:read`. | Herramienta | Devuelve | |---|---| | `get_orders` | Órdenes de conversión, con filtros. No trae una por id: usa los filtros para acotar. | | `get_fills` | Fills (ejecuciones) de **todas** las órdenes — cuando la pregunta abarca varias. | | `get_order_fills` | Fills de **una** orden concreta, cuando ya tienes su id. | ### Contactos Requiere el scope `contacts:read`. | Herramienta | Devuelve | |---|---| | `get_contacts` | Destinos de transferencia guardados (cripto/banco); o uno por `contact_id`. | ### Datos de mercado Requiere el scope `market_data:read`. | Herramienta | Devuelve | |---|---| | `get_assets` | Activos soportados (fiat, stablecoin, cripto, acción) y sus restricciones; o uno por `asset_symbol`. | | `get_markets` | Mercados soportados (pares, mínimos); o uno por `market`. | | `get_indicative_price` | Precio **indicativo** (no vinculante) de un mercado: tasa e importe convertido, para compra o venta. Nada queda reservado, no tiene vencimiento y no devuelve id de orden, así que **no se puede ejecutar**. | :::caution[Un precio indicativo no es una cotización] `get_indicative_price` responde «¿a qué tasa quedaría?», no «resérvame esta tasa». Skipo cobra un spread de dealer, por lo que la tasa **depende del tamaño y no escala linealmente**: pasa `amount` + `amount_asset` para un monto concreto, o omite ambos y el precio será para el **mínimo del mercado** (no lo multipliques para estimar un monto mayor). `amount` está limitado por el `maxIndicativeQuoteAmount` del activo (ver `get_assets`); por encima del límite la llamada se rechaza y el error indica el máximo. Obtener una cotización **confirmable** es `POST /v2/quotes` (scope `trading:write`) y ejecutarla requiere una petición firmada — **ninguna de las dos es accesible desde este conector**. Además es la herramienta **más cara**: **10 puntos** por llamada, frente a 1–3 del resto, porque llega a un proveedor de precios externo. Ver [coste por herramienta](/concepts/rate-limits). ::: ### Estado del servicio Sin scope; sirven para diagnóstico y para anclar fechas relativas. | Herramienta | Devuelve | |---|---| | `check_api_health` | Comprobación de disponibilidad/salud de la API. | | `get_server_time` | Hora del servidor de Skipo (desfase de reloj / fechas relativas). | ## Errores El servidor propaga los errores [RFC 9457](/concepts/errors) de la API v2 al agente: el `code` estable, el `detail` legible y el enlace al [catálogo de errores](/errors). Cuando se alcanza el límite de tasa, el error indica además cuánto hay que esperar. Con eso el asistente puede decidir si vuelve a intentarlo por su cuenta: **Skipo no reintenta la llamada por ti**. --- ## Conector remoto (OAuth) El conector remoto es la forma más rápida de conectar tu cuenta: **no instalas nada**. Tu cliente (por ejemplo Claude) habla con el servidor MCP alojado por Skipo en `https://api.skipo.com/mcp`, y tú autorizas el acceso con tu propia cuenta mediante **OAuth 2.1** — sin manejar ni pegar llaves de API. :::info[Solo lectura] El acceso concedido es **solo de lectura**. El conector no puede mover dinero, crear retiros ni ejecutar conversiones; solo consulta información de tu cuenta. ::: ## Conectar 1. En tu cliente MCP, añade un **conector personalizado** con la URL: ``` https://api.skipo.com/mcp ``` 2. El cliente te redirige a **iniciar sesión con Skipo**. Autentícate con tu cuenta habitual (con verificación 2FA si corresponde). 3. Skipo te muestra una **pantalla de consentimiento** con el acceso de solo lectura que se va a conceder. Revísalo y pulsa **Aprobar**. 4. Vuelves a tu cliente y el conector queda **listo**. Ya puedes preguntar por tus balances, transacciones y mercados en lenguaje natural. ## Qué puede acceder El conector concede acceso de **solo lectura** al mismo conjunto de operaciones `GET` que expone el [servidor MCP](/mcp#herramientas): - Cuenta, balances, historial de balances y movimientos del libro mayor. - Retiros y depósitos. - Órdenes de conversión y sus fills (ejecuciones). - Contactos. - Datos de mercado: activos, mercados y **precios indicativos** (no vinculantes). No se concede ningún acceso de escritura: el conector nunca puede iniciar movimientos de dinero. Un precio indicativo tampoco es una cotización: no reserva nada, no vence y no puede ejecutarse. ## Revocar el acceso Puedes cortar el acceso del conector en cualquier momento desde el panel de Skipo, en **Aplicaciones conectadas**: localiza la conexión y pulsa **Revocar**. A partir de ese momento el conector deja de poder leer tu cuenta y tendrás que volver a autorizarlo si quieres usarlo de nuevo. --- ## Migración desde v1 La API **v2** es una reconstrucción desde cero. Convive con la v1 hasta el **10 de septiembre de 2026**, fecha en la que la v1 se retira. ## Cambios principales ### Autenticación (cambio incompatible) El esquema antiguo (`X-API-KEY` + firma RSA x509) **desaparece** en v2. En su lugar se usa el [esquema de dos niveles](/concepts/authentication): llaves _bearer_ `skp_live_…` para lecturas y JWT firmado con EdDSA para mover dinero. El _onboarding_ es de auto-servicio (ya no se envía un certificado por correo). ### Errores v2 devuelve siempre [`application/problem+json` (RFC 9457)](/concepts/errors) con un `code` estable, en lugar de formatos de error heterogéneos. ### Renombrados de rutas | v1 | v2 | |---|---| | `/convert_orders` | `/orders` | | `/converts` | ⚠️ `/fills` — **una fila por ejecución**, no por orden (ver aviso abajo) | | `/converts/quotations` | `/quotes` | | `/converts/quotations:confirm` | `POST /orders` | | `/currencies` | `/balances` | | `/historical_balances/{currencyId}` | `/balances/{assetSymbol}/history` | | `/ledger_movements/{currencyId}` | `/ledger?assetSymbol=` | | `/supported_currencies` | `/assets` | | `/supported_markets` | `/markets` | | `/users/current` | `/account` | | `/convert_orders/id/{id}` | `GET /orders/{id}` | | `/converts/id/{id}` | ⚠️ sin equivalente 1:1 — ver [Consultas por id](#consultas-por-id-que-cambian-de-forma) | | `/…/clOrdId/{clOrdId}` | ⚠️ sin equivalente — ver [Consultas por id](#consultas-por-id-que-cambian-de-forma) | :::warning[`/converts` es `/fills`, no `/orders`] En v1, `/converts` devuelve **una fila por ejecución** y `/convert_orders` una fila por orden. Su equivalente en v2 es `/fills` y `/orders` respectivamente. Si apuntas a `/orders` lo que hoy lee `/converts`, el número de filas cambia: una orden con varias ejecuciones devuelve una sola fila en `/orders` donde v1 devolvía varias. ::: El recurso se llama **orden**, no "conversión": Convert es la marca del producto, pero el modelo es de intercambio (una orden con un `type`), para que en el futuro se puedan añadir otros tipos de orden sin romper el contrato. ### Consultas por id que cambian de forma Tres rutas de v1 no tienen un equivalente 1:1 en v2: | v1 | v2 | Qué cambia en tu código | |---|---|---| | `/convert_orders/id/{id}` | `GET /v2/orders/{id}` | Nada: sigue devolviendo una fila. | | `/converts/id/{id}` | `GET /v2/orders/{id}/fills` o `GET /v2/fills?orderId={id}` | **Cambia la cardinalidad**: recibes una _lista_ de ejecuciones, no una fila. No existe `GET /v2/fills/{id}`. | | `/convert_orders/clOrdId/{clOrdId}` | — | **No hay búsqueda por `clientOrderId` en v2.** Ese campo sólo se devuelve en la respuesta de `POST /v2/quotes`, junto al `orderId` que sí puedes consultar después. Guarda esa correspondencia en tu lado al cotizar. | :::note[`?sourceId=` sólo existe en el libro contable] `?orderId=` está en `/v2/fills` y `/v2/ledger`. `?sourceId=` está **sólo** en `/v2/ledger` — no filtra órdenes ni ejecuciones. Ver [ids y correlación](/concepts/ids-and-correlation). ::: ### Renombrados de parámetros y campos Los renombrados de rutas se delatan solos con un `404`. Los de parámetros y los de campos de respuesta no se comportan igual entre sí, así que van por separado. #### Parámetros de _query_ — v2 rechaza los que no reconoce v2 **no ignora** un parámetro desconocido: responde `400` [`validation_error`](/errors/validation_error) nombrando el parámetro sobrante (`property take should not exist`). Es deliberado — en una API de dinero un filtro ignorado devuelve el resultado equivocado sin avisar — y en la práctica significa que un renombrado que se te pase **aparece en la primera llamada**, no en la conciliación de fin de mes. | v1 | v2 | |---|---| | `take=` | `limit=` | | `currencyId=` | `assetSymbol=` | | `currencyBase=` / `currencyQuote=` | `baseAsset=` / `quoteAsset=` | #### Campos de respuesta — estos sí fallan en silencio Aquí no hay red: leer un campo que ya no existe da `undefined`, no un error. | v1 | v2 | Si no lo cambias | |---|---|---| | `meta.currentSize` | `pagination.limit` | Los cuatro cambian a la vez, así que un bucle de paginación escrito para v1 no encuentra los totales y para tras la primera página. | | `meta.totalCount` | `pagination.totalItems` | ⬑ | | `meta.totalPages` | `pagination.totalPages` | ⬑ | | `meta.currentPage` | `pagination.page` | ⬑ | | `quantity` | `amount` | Importe nulo. Ojo también al signo — ver abajo. | | `date` | `createdAt` | Fecha nula. | | `currencyId` | `assetSymbol` | Activo nulo. | | `transactionId` | `source.id` (con `source.type`) | ⚠️ **No es `id`.** `id` existe en v1 y en v2 y es otro campo: el de la propia entrada. Ver [ids y correlación](/concepts/ids-and-correlation). | | `qtyCumBase` / `qtyCumQuote` | `filledBaseAmount` / `filledQuoteAmount` | Cantidad ejecutada nula. | | `currencyBase` / `currencyQuote` | `baseAsset` / `quoteAsset` | Mercado sin identificar. | ### Cambios de significado, no de nombre Dos cambios conservan un nombre razonable pero **cambian el valor**. Ningún error los delata: revísalos explícitamente. #### Los importes públicos son magnitudes positivas En v1, `quantity` venía **con signo** (un retiro era negativo). En v2 `amount`, `fee` y `total` son siempre **positivos**: la dirección la da el `type` de la entrada, y el propio recurso (un retiro debita, un depósito acredita). Si sumas movimientos para calcular un neto, el mismo código **da otro resultado**: en v2 los débitos suman en vez de restar. Ramifica sobre `type` antes de acumular. #### `assetFormat` viene por defecto en `rebased` `/orders`, `/fills` y `/withdrawals` aceptan `assetFormat`, y su valor por defecto es **`rebased`** — un cambio deliberado respecto al comportamiento anterior, que devolvía `base`. Para [equities tokenizadas](/concepts/tokenized-equities) eso cambia los importes que ves respecto a v1, sin ninguna señal de error. En los demás activos el multiplicador es `1` y no cambia nada. Es una decisión contable antes que técnica: si tu integración depende de las cifras liquidadas, pide `assetFormat=base` explícitamente. ### Novedades en v2 - **Fills como recurso de primer nivel** — `GET /v2/fills` y `GET /v2/orders/{id}/fills`. - **Libro contable con trazabilidad** — `GET /v2/ledger`, con `source: {type, id}` y filtros `?sourceId=` / `?orderId=` ([guía](/concepts/ids-and-correlation)). - **Precio indicativo** — `GET /v2/markets/{market}/price`, una tasa **no vinculante** que no reserva nada y no requiere firma. (Los listados de mercados y activos ya existían en v1 como `/supported_markets` y `/supported_currencies`; lo nuevo aquí es el precio.) - **Webhooks** firmados ([guía](/concepts/webhooks)). - **Límites de tasa por cuenta y por llave** con cabeceras `RateLimit-*`. ## Calendario **La v1 se retira el 10 de septiembre de 2026** (`2026-09-10`). A partir de esa fecha sus endpoints dejan de atender tráfico: completa la migración a v2 antes de ese día. Antes del apagado, la v1 empezará a devolver cabeceras `Deprecation` / `Sunset` que anuncian esa misma fecha, y se pueden ejecutar _brownouts_ —ventanas breves y anunciadas en las que la v1 devuelve error— para que verifiques que tu migración está completa.