Overview
Invoice intent, checkout activation, payment acceptance, and order fulfillment.
An invoice represents a payment intent. Create one for an order, save its ID, and redirect the customer to its checkout_url.
Create an invoice#
Send a positive decimal amount and currency. For crypto, also send network. For an enabled fiat denomination such as USD, omit network; the customer chooses a payment pair on checkout.
Optional correlation fields are external_id, description, payer_email, redirect_url, and metadata. external_id is unique per merchant/environment and is immutable; a duplicate returns 409 conflict. redirect_url must be HTTPS. It overrides the checkout return destination and is not proof of payment.
Creation requires invoices:write and an idempotency key. See Create for all fields and constraints.
For example, this request creates an invoice priced at USD 25. Use a new idempotency key for each invoice and reuse it for identical retries:
curl --request POST 'https://api.thehalfin.com/v1/invoices' \
--header "X-API-Key: $HALFIN_API_KEY" \
--header 'Idempotency-Key: invoice-overview-1001' \
--header 'Content-Type: application/json' \
--data '{"amount":"25.00","currency":"USD"}'The API uses different names for the original price in the response:
| Field you send | Field you receive in data |
|---|---|
amount | nominal_amount |
currency | nominal_currency |
network for crypto; omitted for fiat | nominal_network; null for fiat |
For the request above, the response includes nominal_amount: "25", nominal_currency: "USD", and nominal_network: null. These are response fields; do not send nominal_* in the request. Decimal formatting may be normalized without changing the value.
Checkout activation#
The response also describes the customer's crypto payment. For a newly created fiat-priced invoice, amount_requested, currency, network, and deposit_address in the response are null until checkout activation. The invoice's returned status is pending throughout this initial stage.
When the customer selects a payment pair and activates checkout, the API fills these response fields and fixes the payment rate. The nominal_* response fields continue to describe the original invoice price, such as USD 25, while amount_requested, currency, and network describe the crypto payment.
Both deadlines are also response fields set by the API. draft_expires_at is the deadline to activate checkout; after activation it is null. expires_at is null before activation and then contains the payment deadline. Read expires_at to display a payment timer; do not start one when creating the invoice. Neither deadline is an input to the create request.
Invoice statuses#
| Status | Meaning |
|---|---|
pending | Waiting for activation or an accepted payment |
confirming | At least one payment accepted, but the total is below the paid threshold |
paid | Accepted amount meets the merchant's payment threshold |
overpaid | Accepted amount exceeds the requested total beyond the overpayment tolerance |
underpaid | Closed with part of the amount accepted: the payment window ended, or a timely payment could not be accepted (possibly before the deadline) |
expired | Payment window closed without an accepted payment; later funds can leave this status unchanged when automatic late completion is disabled |
invalid | An announced payment was dropped or reorged before acceptance |
rejected | A timely payment cannot be accepted, and nothing was accepted on the invoice. The invoice is closed. This status is final, even if money is accepted later (invoice.late_payment) |
A confirmed payment can take longer to be accepted. Meanwhile the invoice stays pending or confirming and does not expire. rejected carries no reason. The status list can grow, so accept a value you do not recognize without failing and do not fulfill on it. See payments that take longer for the transitions.
invoice.detected reports observation before acceptance. invoice.confirming reports accepted funds; it does not mean “first seen in the mempool.” An invoice may have several entries in payments[], each with its own chain status.
Payment settlement#
Every entry in payments[] has a stable id and its own settlement object. settlement reports what that one payment did to your balances — its own ledger facts and the state of its conversion decision — independently of the payment's receipt status and of the invoice status. A confirmed payment on an invoice that reads paid can still be unsettled. settlement is null only where the read did not compute it for that row; a null object is not an empty one.
settlement.conversion_status reports the state of that payment's conversion decision:
conversion_status | What it means for the merchant |
|---|---|
not_applicable | No conversion for this payment. It stays in the asset it arrived in |
pending | A target is frozen and the conversion has not completed. Nothing is credited in the target yet |
settled | This payment's own credit in the target asset is committed |
source_restored | The conversion returned the source asset; the payment stays credited in the asset it arrived in |
needs_review | The conversion is held for an operator. Do not treat it as final either way |
unknown | The recorded facts are incomplete or contradictory. No amount is inferred, and the zeros carry no meaning |
The list can grow. Handle a value you do not recognize the way you handle unknown.
settlement.source is always the gate the payment arrived on. settlement.target is the conversion target frozen for that payment and is null when it has none — including a not_applicable payment on an invoice that did freeze a target. It is never your current conversion setting.
Every amount is a decimal string in its own native asset, never priced at a current rate. accepted_source_amount is this payment's latest acceptance credit in the source asset. source_available_contribution is the signed net of everything this payment booked on your available source balance, so it can be negative after a reversal, and it is not your wallet balance. credited_target_amount is this payment's own committed target credit and is non-zero only when conversion_status is settled. pending_conversion_source_amount is the unfinished source obligation of a pending decision; it is null in every other state and before a decision exists, and a null there means "nothing to report", not zero.
reserved_amounts and reversed_credits list this payment's remaining holds and its reversed credits per gate, and are empty when there is nothing to report. Each entry carries its own currency and network. Never add entries across currencies or networks into one total.
Settlement is derived from the acceptance originals and the conversion facts, never from timestamps and never from the receipt status. A confirmation time, a credit time, or a paid invoice does not make a conversion settled; only the recorded conversion facts do. Use settlement to explain one payment, the invoice status for fulfillment, and the balances endpoint for what you can actually pay out.
Fulfill an order#
After verifying and deduplicating a webhook, get the invoice. Fulfill once per invoice when its current status is paid or overpaid; never on pending, confirming, or rejected. Payment tolerance can make a paid invoice's amount_paid slightly smaller than amount_requested; use the status, not a floating-point comparison.
Late payments and reversals require explicit handling. A late payment can also produce a paid event for the same financial effect. A reversed accepted payment reduces amount_paid and can recalculate the status. See lifecycle and webhook business processes.
Invoice origins and lookup#
source is api, dashboard, or static. For static deposits, static_address_id maps the invoice back to a customer address; checkout_url can be null.
Lists support exact-match filters for status, currency, static_address_id, and external_id. Filters combine; external_id is case-sensitive and does not match prefixes. Keep your own order-to-invoice mapping. There is no invoice cancellation endpoint; refunds are a separate outgoing operation with their own eligibility rules.
Refund visibility#
Invoice GET and list responses include refund_summary independently of the payment status. A paid invoice remains paid after a return. See refund coverage, related reads and polling. The summary is a current read projection; an idempotent invoice-creation replay retains its original response.