Skip to documentationDocumentation
On this page

Create

Convert credited balance from one asset into another.

Creates a manual conversion with ONE idempotent command. There is no quote and no separate confirm step: the request reads one fresh rate publication, freezes the merchant target and its minimum, selects the route and its execution bounds, reserves the source, and returns the conversion. The provider's own preparation is a technical artifact of execution — it is not a quote, it cannot be confirmed, and it never rewrites the frozen target.

The idempotency key travels in the Idempotency-Key header and is permanent: replaying it returns the original conversion, and reusing it with a different body is a conflict, never a second conversion.

Conversion exists in the live contour only: the same well-formed request with a test API key is answered 422 with code not_live, not 400 — nothing in the body is wrong, the contour simply has no conversion.

Deterministic target_rounds_to_zero and fee_exceeds_amount refusals release the acquired key and carry Idempotency-Key-Released: true. Other refusals release a key only when that header is present. Conflicts with an existing or in-flight key do not release that key.

A source balance below the requested amount is refused synchronously with 422 insufficient_balance and nothing is created. The balance is re-checked when the source is debited, so a balance spent concurrently between the two is refused with the same 422; nothing is stored and the idempotency key is released for the merchant's next attempt.

Usage notes#

Requires balances:manage. The API key selects the merchant and environment. Persist the idempotency key before sending. source_amount includes any merchant-paid fee; it is not principal plus an extra fee. A successful response accepts the operation and does not by itself prove the target was credited. Save operation_id and reconcile through Get.

Live-only: a test API key returns 422 not_live. See conversion lifecycle and webhooks for completion, failure, recovery, and ID reconciliation.

POST/v1/conversions
curl
curl --request POST 'https://api.thehalfin.com/v1/conversions' \
  --header "X-API-Key: $HALFIN_API_KEY" \
  --header 'Idempotency-Key: createManualConversion-example-1001' \
  --header 'Content-Type: application/json' \
  --data '{
  "source_currency": "USDC",
  "source_network": "ethereum",
  "target_currency": "USDT",
  "target_network": "tron",
  "source_amount": "100.000000"
}'

Authorization

  • X-API-KeystringRequired
    API key issued by the merchant dashboard. The key's environment (live/test) determines which data the caller can access. Permissions on the key control which endpoints are reachable.

    Example: $HALFIN_API_KEY

Header Parameters

  • Idempotency-KeystringRequired

    Persist a unique key before sending this operation. Retry an uncertain outcome with the same key and unchanged request; a new operation needs a new key.

    See idempotency and retries for replay, conflict, and key-release behavior.

    Full header contract

    Idempotency-Key is required for POST operations that create entities, trigger external effects or record decisions, and for audited platform configuration mutations. Auth/session, checkout token operations, public contact submission, pure previews and ordinary existing-row edits are exempt. The fingerprint is SHA-256 over canonical path parameters, query parameters and the entire body. JSON keys are sorted and insignificant whitespace is ignored; numeric literals and string values are preserved ("10" differs from "10.00"). Non-JSON bodies, including multipart, use raw bytes; an absent body is empty. Changing any field under a bound key returns 409 conflict. An executing key returns 409 in_flight. Outcomes are permanent. An exact retry returns the original outcome with Idempotent-Replayed: true. A proven no-effect refusal releases the key and carries Idempotency-Key-Released: true; an unconfirmed release returns 503 service_unavailable without that header. Under uncertainty retry the original request with the same key. The key is never echoed in response bodies. Keys contain 1–255 printable ASCII characters without spaces; UUID is recommended. Merchant operations share the (merchant_id, environment, key) namespace; untenanted operations share the platform namespace.

    • Minimum length: 1
    • Maximum length: 255
    • Pattern: ^[\x21-\x7E]+$

    Example: createManualConversion-example-1001

Request Body

Required

application/json

  • source_currencystringRequired
    Source balance currency code.
    • Maximum length: 20

    Example: string

  • source_networkstringRequired
    Source balance network.
    • Maximum length: 64

    Example: string

  • target_currencystringRequired
    Target currency code.
    • Maximum length: 20

    Example: string

  • target_networkstringRequired
    Target network.
    • Maximum length: 64

    Example: string

  • source_amountstringRequired
    Decimal string source amount. It is reserved out of the available balance by this request; the target is credited only when the execution reaches a terminal, evidenced result.
    • Maximum length: 50

    Example: 1

Response Body

201 Accepted manual conversion.

  • Idempotent-Replayed"true"Optional
    Present and true when returning the stored outcome of an earlier execution.

    Example: true

  • Idempotency-Key-Released"true"Optional
    Present and true after a proven no-effect refusal and confirmed release. This attempt bound nothing; a concurrent request may acquire the key.

    Example: true

application/json

  • dataobjectRequired
    • Unknown properties are rejected

    Example: {"updated_at":"2026-01-01T00:00:00Z","operation_id":"00000000-0000-0000-0000-000000000001","status":"pending_execution","source_currency":"string","source_network":"string","target_currency":"string","target_network":"string","source_amount":"string","expected_target_amount":"string","effective_rate":"string","conversion_amount":"string","fee_amount":"string","fee_currency":"string","fee_network":"string","fee_payer":"merchant","converted_amount":"string","fee_charged_amount":"string","source_returned_amount":"string","credited_target_amount":"string","created_at":"2026-01-01T00:00:00Z","settled_at":null,"source_restored_at":null,"stuck_at":null}

    • updated_atstringRequired
      • Format: date-time

      Example: 2026-01-01T00:00:00Z

    • operation_idstringRequired
      Standalone conversion ID, equal to webhook conversion_id. Use with /v1/conversions/{operationID}.
      • Format: uuid

      Example: 00000000-0000-0000-0000-000000000001

    • status"pending_execution" | "submitted" | "settled" | "failed" | "source_restored" | "stuck"Required
      Merchant lifecycle status. failed is recoverable and may later become settled or source_restored. submitted and stuck are merchant-facing states; the admin ops_status is a separate operational projection, including pending_execution and ops_review_required.

      Example: pending_execution

    • source_currencystringRequired

      Example: string

    • source_networkstringRequired

      Example: string

    • target_currencystringRequired

      Example: string

    • target_networkstringRequired

      Example: string

    • source_amountstringRequired

      Example: string

    • expected_target_amountstringRequired

      Example: string

    • effective_ratestringRequired

      Example: string

    • conversion_amountstringOptional
      The principal actually exchanged: source_amount minus the merchant-paid fee. The fee is deducted from the amount and never added on top, so source_amount is the ceiling on what this operation may debit.

      Example: string

    • fee_amountstringOptional
      The fee withheld from source_amount when the whole principal is exchanged. "0" when the platform covers it, because nothing is then withheld from the merchant.

      Example: string

    • fee_currencystringOptional
      Rail of the fee. Always the source rail.

      Example: string

    • fee_networkstringOptional

      Example: string

    • fee_payer"merchant" | "platform"Optional

      Example: merchant

    • converted_amountstringOptional
      Principal the execution actually consumed. "0" until the ledger books it.

      Example: string

    • fee_charged_amountstringOptional
      Fee actually charged. "0" until the ledger books it.

      Example: string

    • source_returned_amountstringOptional
      Unspent source released back to the available balance. "0" until the ledger books it.

      Example: string

    • credited_target_amountstringOptional
      Target amount actually credited to the merchant. It is the frozen obligation scaled by the consumed share, not the amount the venue delivered — gain and loss against it are the platform's position. "0" until the ledger books it.

      Example: string

    • created_atstringRequired
      • Format: date-time

      Example: 2026-01-01T00:00:00Z

    • settled_atstring | nullRequired
      • Format: date-time

      Example: null

    • source_restored_atstring | nullRequired
      • Format: date-time

      Example: null

    • stuck_atstring | nullRequired
      • Format: date-time

      Example: null

  • metaobjectRequired
    • Unknown properties are rejected

    Example: {"request_id":"00000000-0000-0000-0000-000000000001"}

    • request_idstringRequired
      Correlation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.

      Example: string

201 response
{
  "data": {
    "updated_at": "2026-01-01T00:00:00Z",
    "operation_id": "00000000-0000-0000-0000-000000000001",
    "status": "pending_execution",
    "source_currency": "string",
    "source_network": "string",
    "target_currency": "string",
    "target_network": "string",
    "source_amount": "string",
    "expected_target_amount": "string",
    "effective_rate": "string",
    "conversion_amount": "string",
    "fee_amount": "string",
    "fee_currency": "string",
    "fee_network": "string",
    "fee_payer": "merchant",
    "converted_amount": "string",
    "fee_charged_amount": "string",
    "source_returned_amount": "string",
    "credited_target_amount": "string",
    "created_at": "2026-01-01T00:00:00Z",
    "settled_at": null,
    "source_restored_at": null,
    "stuck_at": null
  },
  "meta": {
    "request_id": "00000000-0000-0000-0000-000000000001"
  }
}

400 `validation_error`: The request format or field values are invalid. `target_rounds_to_zero`: The target amount rounds to zero. `fee_exceeds_amount`: The conversion fee consumes the source amount.

  • Idempotent-Replayed"true"Optional
    Present and true when returning the stored outcome of an earlier execution.

    Example: true

  • Idempotency-Key-Released"true"Optional
    Present and true after a proven no-effect refusal and confirmed release. This attempt bound nothing; a concurrent request may acquire the key.

    Example: true

application/json

  • errorobjectRequired
    The error code and diagnostic message.
    • Unknown properties are rejected

    Example: {"code":"validation_error","message":"string","details":["string"]}

    • code"validation_error" | "target_rounds_to_zero" | "fee_exceeds_amount"Required
      `validation_error`: The request format or field values are invalid. `target_rounds_to_zero`: The target amount rounds to zero. `fee_exceeds_amount`: The conversion fee consumes the source amount.

      Example: validation_error

    • messagestringRequired
      Human-readable description of the error.

      Example: string

    • detailsarray<string>Optional
      Optional structured context (validation errors, etc.).

      Example: ["string"]

  • metaobjectRequired
    • Unknown properties are rejected

    Example: {"request_id":"00000000-0000-0000-0000-000000000001"}

    • request_idstringRequired
      Correlation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.

      Example: string

400 response
{
  "error": {
    "code": "validation_error",
    "message": "Request validation failed"
  },
  "meta": {
    "request_id": "00000000-0000-0000-0000-000000000001"
  }
}

401 `unauthorized`: A valid API key is required.

  • Idempotent-Replayed"true"Optional
    Present and true when returning the stored outcome of an earlier execution.

    Example: true

  • Idempotency-Key-Released"true"Optional
    Present and true after a proven no-effect refusal and confirmed release. This attempt bound nothing; a concurrent request may acquire the key.

    Example: true

application/json

  • errorobjectRequired
    The error code and diagnostic message.
    • Unknown properties are rejected

    Example: {"code":"unauthorized","message":"string","details":["string"]}

    • code"unauthorized"Required
      `unauthorized`: A valid API key is required.

      Example: unauthorized

    • messagestringRequired
      Human-readable description of the error.

      Example: string

    • detailsarray<string>Optional
      Optional structured context (validation errors, etc.).

      Example: ["string"]

  • metaobjectRequired
    • Unknown properties are rejected

    Example: {"request_id":"00000000-0000-0000-0000-000000000001"}

    • request_idstringRequired
      Correlation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.

      Example: string

401 response
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid API key"
  },
  "meta": {
    "request_id": "00000000-0000-0000-0000-000000000001"
  }
}

403 `forbidden`: The API key does not have permission for this operation. `api_key_ip_forbidden`: The request source address is not allowed by the API key.

  • Idempotent-Replayed"true"Optional
    Present and true when returning the stored outcome of an earlier execution.

    Example: true

  • Idempotency-Key-Released"true"Optional
    Present and true after a proven no-effect refusal and confirmed release. This attempt bound nothing; a concurrent request may acquire the key.

    Example: true

application/json

  • errorobjectRequired
    The error code and diagnostic message.
    • Unknown properties are rejected

    Example: {"code":"forbidden","message":"string","details":["string"]}

    • code"forbidden" | "api_key_ip_forbidden"Required
      `forbidden`: The API key does not have permission for this operation. `api_key_ip_forbidden`: The request source address is not allowed by the API key.

      Example: forbidden

    • messagestringRequired
      Human-readable description of the error.

      Example: string

    • detailsarray<string>Optional
      Optional structured context (validation errors, etc.).

      Example: ["string"]

  • metaobjectRequired
    • Unknown properties are rejected

    Example: {"request_id":"00000000-0000-0000-0000-000000000001"}

    • request_idstringRequired
      Correlation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.

      Example: string

403 response
{
  "error": {
    "code": "forbidden",
    "message": "Permission denied"
  },
  "meta": {
    "request_id": "00000000-0000-0000-0000-000000000001"
  }
}

409 `conflict`: This idempotency key is bound to another request. `in_flight`: The original request is still executing. `daily_cap_exceeded`: The conversion route has spent its daily volume cap.

  • Idempotent-Replayed"true"Optional
    Present and true when returning the stored outcome of an earlier execution.

    Example: true

  • Idempotency-Key-Released"true"Optional
    Present and true after a proven no-effect refusal and confirmed release. This attempt bound nothing; a concurrent request may acquire the key.

    Example: true

application/json

  • errorobjectRequired
    The error code and diagnostic message.
    • Unknown properties are rejected

    Example: {"code":"conflict","message":"string","details":["string"]}

    • code"conflict" | "in_flight" | "daily_cap_exceeded"Required
      `conflict`: This idempotency key is bound to another request. `in_flight`: The original request is still executing. `daily_cap_exceeded`: The conversion route has spent its daily volume cap.

      Example: conflict

    • messagestringRequired
      Human-readable description of the error.

      Example: string

    • detailsarray<string>Optional
      Optional structured context (validation errors, etc.).

      Example: ["string"]

  • metaobjectRequired
    • Unknown properties are rejected

    Example: {"request_id":"00000000-0000-0000-0000-000000000001"}

    • request_idstringRequired
      Correlation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.

      Example: string

409 response
{
  "error": {
    "code": "conflict",
    "message": "Request conflicts with the existing state"
  },
  "meta": {
    "request_id": "00000000-0000-0000-0000-000000000001"
  }
}

422 `not_live`: Manual conversion is available with live API keys only. `unprocessable_entity`: The conversion requirements are not satisfied. `gate_not_enabled`: The currency and network are not enabled for this merchant. `route_limit_exceeded`: The requested amount exceeds the conversion route limit. `insufficient_balance`: The available source balance is insufficient.

  • Idempotent-Replayed"true"Optional
    Present and true when returning the stored outcome of an earlier execution.

    Example: true

  • Idempotency-Key-Released"true"Optional
    Present and true after a proven no-effect refusal and confirmed release. This attempt bound nothing; a concurrent request may acquire the key.

    Example: true

application/json

  • errorobjectRequired
    The error code and diagnostic message.
    • Unknown properties are rejected

    Example: {"code":"not_live","message":"string","details":["string"]}

    • code"not_live" | "unprocessable_entity" | "gate_not_enabled" | "route_limit_exceeded" | "insufficient_balance"Required
      `not_live`: Manual conversion is available with live API keys only. `unprocessable_entity`: The conversion requirements are not satisfied. `gate_not_enabled`: The currency and network are not enabled for this merchant. `route_limit_exceeded`: The requested amount exceeds the conversion route limit. `insufficient_balance`: The available source balance is insufficient.

      Example: not_live

    • messagestringRequired
      Human-readable description of the error.

      Example: string

    • detailsarray<string>Optional
      Optional structured context (validation errors, etc.).

      Example: ["string"]

  • metaobjectRequired
    • Unknown properties are rejected

    Example: {"request_id":"00000000-0000-0000-0000-000000000001"}

    • request_idstringRequired
      Correlation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.

      Example: string

422 response
{
  "error": {
    "code": "unprocessable_entity",
    "message": "The request cannot be processed in the current state"
  },
  "meta": {
    "request_id": "00000000-0000-0000-0000-000000000001"
  }
}

500 `internal_error`: The request could not be completed. Contact support with the request ID.

  • Idempotent-Replayed"true"Optional
    Present and true when returning the stored outcome of an earlier execution.

    Example: true

  • Idempotency-Key-Released"true"Optional
    Present and true after a proven no-effect refusal and confirmed release. This attempt bound nothing; a concurrent request may acquire the key.

    Example: true

application/json

  • errorobjectRequired
    The error code and diagnostic message.
    • Unknown properties are rejected

    Example: {"code":"internal_error","message":"string","details":["string"]}

    • code"internal_error"Required
      `internal_error`: The request could not be completed. Contact support with the request ID.

      Example: internal_error

    • messagestringRequired
      Human-readable description of the error.

      Example: string

    • detailsarray<string>Optional
      Optional structured context (validation errors, etc.).

      Example: ["string"]

  • metaobjectRequired
    • Unknown properties are rejected

    Example: {"request_id":"00000000-0000-0000-0000-000000000001"}

    • request_idstringRequired
      Correlation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.

      Example: string

500 response
{
  "error": {
    "code": "internal_error",
    "message": "Internal server error"
  },
  "meta": {
    "request_id": "00000000-0000-0000-0000-000000000001"
  }
}

503 `service_unavailable`: The service is temporarily unavailable. `pricing_unavailable`: A fresh conversion price is unavailable. `route_unavailable`: An eligible conversion route is unavailable.

  • Idempotency-Key-Released"true"Optional
    Present and true after a proven no-effect refusal and confirmed release. This attempt bound nothing; a concurrent request may acquire the key.

    Example: true

  • Idempotent-Replayed"true"Optional
    Present and true when returning the stored outcome of an earlier execution.

    Example: true

application/json

  • errorobjectRequired
    The error code and diagnostic message.
    • Unknown properties are rejected

    Example: {"code":"service_unavailable","message":"string","details":["string"]}

    • code"service_unavailable" | "pricing_unavailable" | "route_unavailable"Required
      `service_unavailable`: The service is temporarily unavailable. `pricing_unavailable`: A fresh conversion price is unavailable. `route_unavailable`: An eligible conversion route is unavailable.

      Example: service_unavailable

    • messagestringRequired
      Human-readable description of the error.

      Example: string

    • detailsarray<string>Optional
      Optional structured context (validation errors, etc.).

      Example: ["string"]

  • metaobjectRequired
    • Unknown properties are rejected

    Example: {"request_id":"00000000-0000-0000-0000-000000000001"}

    • request_idstringRequired
      Correlation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.

      Example: string

503 response
{
  "error": {
    "code": "service_unavailable",
    "message": "Service temporarily unavailable"
  },
  "meta": {
    "request_id": "00000000-0000-0000-0000-000000000001"
  }
}