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.
/v1/conversionscurl --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-KeystringRequiredAPI 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-KeystringRequiredPersist 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_currencystringRequiredSource balance currency code.- Maximum length: 20
Example:
stringsource_networkstringRequiredSource balance network.- Maximum length: 64
Example:
stringtarget_currencystringRequiredTarget currency code.- Maximum length: 20
Example:
stringtarget_networkstringRequiredTarget network.- Maximum length: 64
Example:
stringsource_amountstringRequiredDecimal 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"OptionalPresent and true when returning the stored outcome of an earlier execution.Example:
trueIdempotency-Key-Released"true"OptionalPresent 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:00Zoperation_idstringRequiredStandalone conversion ID, equal to webhook conversion_id. Use with /v1/conversions/{operationID}.- Format: uuid
Example:
00000000-0000-0000-0000-000000000001status"pending_execution" | "submitted" | "settled" | "failed" | "source_restored" | "stuck"RequiredMerchant 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_executionsource_currencystringRequiredExample:
stringsource_networkstringRequiredExample:
stringtarget_currencystringRequiredExample:
stringtarget_networkstringRequiredExample:
stringsource_amountstringRequiredExample:
stringexpected_target_amountstringRequiredExample:
stringeffective_ratestringRequiredExample:
stringconversion_amountstringOptionalThe 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:
stringfee_amountstringOptionalThe 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:
stringfee_currencystringOptionalRail of the fee. Always the source rail.Example:
stringfee_networkstringOptionalExample:
stringfee_payer"merchant" | "platform"OptionalExample:
merchantconverted_amountstringOptionalPrincipal the execution actually consumed. "0" until the ledger books it.Example:
stringfee_charged_amountstringOptionalFee actually charged. "0" until the ledger books it.Example:
stringsource_returned_amountstringOptionalUnspent source released back to the available balance. "0" until the ledger books it.Example:
stringcredited_target_amountstringOptionalTarget 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:
stringcreated_atstringRequired- Format: date-time
Example:
2026-01-01T00:00:00Zsettled_atstring | nullRequired- Format: date-time
Example:
nullsource_restored_atstring | nullRequired- Format: date-time
Example:
nullstuck_atstring | nullRequired- Format: date-time
Example:
null
metaobjectRequired- Unknown properties are rejected
Example:
{"request_id":"00000000-0000-0000-0000-000000000001"}request_idstringRequiredCorrelation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.Example:
string
{
"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"OptionalPresent and true when returning the stored outcome of an earlier execution.Example:
trueIdempotency-Key-Released"true"OptionalPresent 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
errorobjectRequiredThe 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_errormessagestringRequiredHuman-readable description of the error.Example:
stringdetailsarray<string>OptionalOptional structured context (validation errors, etc.).Example:
["string"]
metaobjectRequired- Unknown properties are rejected
Example:
{"request_id":"00000000-0000-0000-0000-000000000001"}request_idstringRequiredCorrelation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.Example:
string
{
"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"OptionalPresent and true when returning the stored outcome of an earlier execution.Example:
trueIdempotency-Key-Released"true"OptionalPresent 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
errorobjectRequiredThe 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:
unauthorizedmessagestringRequiredHuman-readable description of the error.Example:
stringdetailsarray<string>OptionalOptional structured context (validation errors, etc.).Example:
["string"]
metaobjectRequired- Unknown properties are rejected
Example:
{"request_id":"00000000-0000-0000-0000-000000000001"}request_idstringRequiredCorrelation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.Example:
string
{
"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"OptionalPresent and true when returning the stored outcome of an earlier execution.Example:
trueIdempotency-Key-Released"true"OptionalPresent 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
errorobjectRequiredThe 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:
forbiddenmessagestringRequiredHuman-readable description of the error.Example:
stringdetailsarray<string>OptionalOptional structured context (validation errors, etc.).Example:
["string"]
metaobjectRequired- Unknown properties are rejected
Example:
{"request_id":"00000000-0000-0000-0000-000000000001"}request_idstringRequiredCorrelation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.Example:
string
{
"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"OptionalPresent and true when returning the stored outcome of an earlier execution.Example:
trueIdempotency-Key-Released"true"OptionalPresent 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
errorobjectRequiredThe 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:
conflictmessagestringRequiredHuman-readable description of the error.Example:
stringdetailsarray<string>OptionalOptional structured context (validation errors, etc.).Example:
["string"]
metaobjectRequired- Unknown properties are rejected
Example:
{"request_id":"00000000-0000-0000-0000-000000000001"}request_idstringRequiredCorrelation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.Example:
string
{
"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"OptionalPresent and true when returning the stored outcome of an earlier execution.Example:
trueIdempotency-Key-Released"true"OptionalPresent 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
errorobjectRequiredThe 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_livemessagestringRequiredHuman-readable description of the error.Example:
stringdetailsarray<string>OptionalOptional structured context (validation errors, etc.).Example:
["string"]
metaobjectRequired- Unknown properties are rejected
Example:
{"request_id":"00000000-0000-0000-0000-000000000001"}request_idstringRequiredCorrelation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.Example:
string
{
"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"OptionalPresent and true when returning the stored outcome of an earlier execution.Example:
trueIdempotency-Key-Released"true"OptionalPresent 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
errorobjectRequiredThe 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_errormessagestringRequiredHuman-readable description of the error.Example:
stringdetailsarray<string>OptionalOptional structured context (validation errors, etc.).Example:
["string"]
metaobjectRequired- Unknown properties are rejected
Example:
{"request_id":"00000000-0000-0000-0000-000000000001"}request_idstringRequiredCorrelation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.Example:
string
{
"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"OptionalPresent and true after a proven no-effect refusal and confirmed release. This attempt bound nothing; a concurrent request may acquire the key.Example:
trueIdempotent-Replayed"true"OptionalPresent and true when returning the stored outcome of an earlier execution.Example:
true
application/json
errorobjectRequiredThe 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_unavailablemessagestringRequiredHuman-readable description of the error.Example:
stringdetailsarray<string>OptionalOptional structured context (validation errors, etc.).Example:
["string"]
metaobjectRequired- Unknown properties are rejected
Example:
{"request_id":"00000000-0000-0000-0000-000000000001"}request_idstringRequiredCorrelation identifier issued by the API, also returned in the X-Request-ID response header. Client-supplied X-Request-ID values are replaced.Example:
string
{
"error": {
"code": "service_unavailable",
"message": "Service temporarily unavailable"
},
"meta": {
"request_id": "00000000-0000-0000-0000-000000000001"
}
}