Skip to content

Create a hosted crypto invoice

POST
/billing/topups
curl --request POST \
--url https://api.flotera.com/api/v1/billing/topups \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: example' \
--cookie __Host-erm_session=<__Host-erm_session> \
--data '{ "amount_usd": "example", "payment_method": "example", "purpose": { "kind": "wallet", "target_plan": "example", "change_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0" } }'

Creates a top-up with the payment provider and returns the hosted checkout_url. Allowed while the account is in grace or frozen: a top-up is the only way out of a freeze (14 §3.6), so denying it would make the state unrecoverable.

The balance moves only on a verified terminal finished IPN, for the amount actually confirmed (D-13). Never treat the browser redirect as success — poll GET /billing/topups/{topup_id} and accept only status=succeeded.

Retrying with the same Idempotency-Key returns the same top-up and the same invoice; it never creates a second payment.

Idempotency-Key
required
string
>= 1 characters <= 128 characters /^[\x21-\x7E]{1,128}$/

Idempotency key, 1–128 ASCII, unique per (tenant_id, operation, key) for at least 24 hours. Retrying with the same body replays the stored response; a different body returns 409 idempotency_conflict.

Media typeapplication/json
object
amount_usd
required

Top-up amount in USD as a decimal string. Must be at least the confirmed minimum of the chosen method (GET /billing/payment-methods).

string
/^[0-9]+(\.[0-9]{1,6})?$/
payment_method
required

Code from the server-owned catalogue, e.g. card or usdt_tron. The catalogue depends on which providers the operator has enabled, so a code that worked yesterday may be absent today — always take the list from GET /billing/payment-methods.

string
<= 32 characters /^[a-z][a-z0-9_]{1,31}$/
purpose
Any of:

What the money is for. wallet — plain balance top-up; subscription — the payment that activates a pending upgrade, in which case the activation happens inside the verified payment transaction and never from a browser redirect (14 §3.2).

object
kind
required
string
Allowed values: wallet subscription
target_plan
Any of:

Internal catalog code. Not the public label — labels are not unique.

string
<= 64 characters /^[a-z][a-z0-9_-]{1,63}$/
change_id
Any of:

UUID (v7 for new entities; v4 accepted during migration).

string format: uuid

Top-up created; open checkout_url as external navigation.

Media typeapplication/json
object
id
required

UUID (v7 for new entities; v4 accepted during migration).

string format: uuid
status
required

Internal top-up state (14 §6.3). Exactly one of them means the money arrived: succeeded.

partially_paid is progress information — the customer under-paid but the payment is still moving. It does not park the top-up for an operator and it does not move the balance (D-13).

review means invoice/order/currency disagreement, an unknown provider status or an ambiguous create — a human has to look.

string
Allowed values: creating provider_unknown waiting_for_customer confirming confirmed settling partially_paid succeeded failed expired reversed review
amount_usd
required

Invoiced amount. What was credited is the confirmed amount, not this.

string
/^[0-9]+(\.[0-9]{1,6})?$/
payment_method
required

Code from the server-owned catalogue, e.g. card or usdt_tron. The catalogue depends on which providers the operator has enabled, so a code that worked yesterday may be absent today — always take the list from GET /billing/payment-methods.

string
<= 32 characters /^[a-z][a-z0-9_]{1,31}$/
provider
required
string
<= 32 characters
checkout_url

Hosted provider page. Open it as external navigation — never inject it as HTML. Absent once the top-up reaches a terminal state.

string | null format: uri
<= 2048 characters
expires_at

RFC 3339 UTC with microsecond precision, or null.

string | null format: date-time
created_at
required

RFC 3339 UTC with microsecond precision.

string format: date-time
Example
{
"status": "creating",
"created_at": "2026-07-25T10:15:30.123456Z"
}

Missing/invalid credentials (code=unauthorized).

Media typeapplication/problem+json

RFC 9457 problem document with a stable machine code.

object
type
required
string format: uri
title
required
string
<= 256 characters
status
required
integer
>= 100 <= 599
code
required

Stable machine-readable error code (03 §2.4).

string
Allowed values: unauthorized forbidden csrf_rejected not_found conflict idempotency_conflict validation_failed rate_limited payload_too_large unsupported_agent_version temporarily_unavailable offline_queue_full online_queue_full plan_required server_limit_reached feature_not_entitled account_in_grace account_frozen payment_pending payment_expired payment_amount_mismatch change_already_pending change_already_applied change_effective reserve_not_covered direction_changed
detail
string
<= 2048 characters
instance
string
<= 512 characters
request_id

UUID (v7 for new entities; v4 accepted during migration).

string format: uuid
errors
Array<object>
<= 100 items
object
path
required

JSON Pointer to the offending field.

string
<= 512 characters
code
required
string
<= 64 characters
Example
{
"type": "https://ermeon.com/problems/validation",
"code": "unauthorized"
}

Authenticated but not permitted. code=forbidden — role/scope mismatch; code=feature_not_entitled — the plan does not include the capability; code=account_frozen — the account is frozen and this operation is declared x-ermeon-frozen: deny. The three are deliberately distinct: only the last one is fixed by a top-up (13 §5.6).

Media typeapplication/problem+json

RFC 9457 problem document with a stable machine code.

object
type
required
string format: uri
title
required
string
<= 256 characters
status
required
integer
>= 100 <= 599
code
required

Stable machine-readable error code (03 §2.4).

string
Allowed values: unauthorized forbidden csrf_rejected not_found conflict idempotency_conflict validation_failed rate_limited payload_too_large unsupported_agent_version temporarily_unavailable offline_queue_full online_queue_full plan_required server_limit_reached feature_not_entitled account_in_grace account_frozen payment_pending payment_expired payment_amount_mismatch change_already_pending change_already_applied change_effective reserve_not_covered direction_changed
detail
string
<= 2048 characters
instance
string
<= 512 characters
request_id

UUID (v7 for new entities; v4 accepted during migration).

string format: uuid
errors
Array<object>
<= 100 items
object
path
required

JSON Pointer to the offending field.

string
<= 512 characters
code
required
string
<= 64 characters
Example
{
"type": "https://ermeon.com/problems/validation",
"code": "unauthorized"
}

State conflict (code=conflict, idempotency_conflict, offline_queue_full or online_queue_full).

Media typeapplication/problem+json

RFC 9457 problem document with a stable machine code.

object
type
required
string format: uri
title
required
string
<= 256 characters
status
required
integer
>= 100 <= 599
code
required

Stable machine-readable error code (03 §2.4).

string
Allowed values: unauthorized forbidden csrf_rejected not_found conflict idempotency_conflict validation_failed rate_limited payload_too_large unsupported_agent_version temporarily_unavailable offline_queue_full online_queue_full plan_required server_limit_reached feature_not_entitled account_in_grace account_frozen payment_pending payment_expired payment_amount_mismatch change_already_pending change_already_applied change_effective reserve_not_covered direction_changed
detail
string
<= 2048 characters
instance
string
<= 512 characters
request_id

UUID (v7 for new entities; v4 accepted during migration).

string format: uuid
errors
Array<object>
<= 100 items
object
path
required

JSON Pointer to the offending field.

string
<= 512 characters
code
required
string
<= 64 characters
Example
{
"type": "https://ermeon.com/problems/validation",
"code": "unauthorized"
}

Request failed validation (code=validation_failed), with per-field errors.

Media typeapplication/problem+json

RFC 9457 problem document with a stable machine code.

object
type
required
string format: uri
title
required
string
<= 256 characters
status
required
integer
>= 100 <= 599
code
required

Stable machine-readable error code (03 §2.4).

string
Allowed values: unauthorized forbidden csrf_rejected not_found conflict idempotency_conflict validation_failed rate_limited payload_too_large unsupported_agent_version temporarily_unavailable offline_queue_full online_queue_full plan_required server_limit_reached feature_not_entitled account_in_grace account_frozen payment_pending payment_expired payment_amount_mismatch change_already_pending change_already_applied change_effective reserve_not_covered direction_changed
detail
string
<= 2048 characters
instance
string
<= 512 characters
request_id

UUID (v7 for new entities; v4 accepted during migration).

string format: uuid
errors
Array<object>
<= 100 items
object
path
required

JSON Pointer to the offending field.

string
<= 512 characters
code
required
string
<= 64 characters
Example
{
"type": "https://ermeon.com/problems/validation",
"code": "unauthorized"
}

Rate limit exceeded (code=rate_limited).

Media typeapplication/problem+json

RFC 9457 problem document with a stable machine code.

object
type
required
string format: uri
title
required
string
<= 256 characters
status
required
integer
>= 100 <= 599
code
required

Stable machine-readable error code (03 §2.4).

string
Allowed values: unauthorized forbidden csrf_rejected not_found conflict idempotency_conflict validation_failed rate_limited payload_too_large unsupported_agent_version temporarily_unavailable offline_queue_full online_queue_full plan_required server_limit_reached feature_not_entitled account_in_grace account_frozen payment_pending payment_expired payment_amount_mismatch change_already_pending change_already_applied change_effective reserve_not_covered direction_changed
detail
string
<= 2048 characters
instance
string
<= 512 characters
request_id

UUID (v7 for new entities; v4 accepted during migration).

string format: uuid
errors
Array<object>
<= 100 items
object
path
required

JSON Pointer to the offending field.

string
<= 512 characters
code
required
string
<= 64 characters
Example
{
"type": "https://ermeon.com/problems/validation",
"code": "unauthorized"
}
Retry-After
integer

Seconds to wait before retrying.

A dependency the endpoint needs is not configured or is temporarily unavailable (code=unavailable). The rest of the API keeps working — an absent external integration must not take the service down.

Media typeapplication/problem+json

RFC 9457 problem document with a stable machine code.

object
type
required
string format: uri
title
required
string
<= 256 characters
status
required
integer
>= 100 <= 599
code
required

Stable machine-readable error code (03 §2.4).

string
Allowed values: unauthorized forbidden csrf_rejected not_found conflict idempotency_conflict validation_failed rate_limited payload_too_large unsupported_agent_version temporarily_unavailable offline_queue_full online_queue_full plan_required server_limit_reached feature_not_entitled account_in_grace account_frozen payment_pending payment_expired payment_amount_mismatch change_already_pending change_already_applied change_effective reserve_not_covered direction_changed
detail
string
<= 2048 characters
instance
string
<= 512 characters
request_id

UUID (v7 for new entities; v4 accepted during migration).

string format: uuid
errors
Array<object>
<= 100 items
object
path
required

JSON Pointer to the offending field.

string
<= 512 characters
code
required
string
<= 64 characters
Example
{
"type": "https://ermeon.com/problems/validation",
"code": "unauthorized"
}