Skip to content
Login

Transfers ​

A transfer binds one unexpired quote option to a saved fiat account or crypto wallet. Heron tracks the overall transfer and each provider leg through normalized status and progress fields.

Create a transfer ​

http
POST /v1/transfers
Idempotency-Key: <stable-key-for-this-logical-transfer>

The required body fields are quoteOptionId and destination:

json
{
  "quoteOptionId": "00000000-0000-4000-8000-000000000010",
  "destination": {
    "type": "FIAT_ACCOUNT",
    "fiatAccountId": "00000000-0000-4000-8000-000000000020"
  },
  "entityId": "00000000-0000-4000-8000-000000000001",
  "transferPurpose": "SERVICES",
  "clientReference": "invoice-1042"
}

For a crypto destination, use:

json
{
  "type": "CRYPTO_WALLET",
  "cryptoWalletId": "00000000-0000-4000-8000-000000000030"
}

clientReference is optional customer context. It does not replace Idempotency-Key.

Transfer purpose ​

Read transferPurposeRequirement from the selected quote option. When required is true, send exactly one value from its allowedValues array as transferPurpose. Heron validates the value against the quote before creating any provider transfer and maps the canonical value to each provider's native code during execution.

The canonical values are:

ADVERTISING, BUSINESS_OPERATIONS, CHARITABLE_DONATION, EDUCATION, FAMILY_SUPPORT, GIFT, GOODS, INSURANCE, INTERNAL_TRANSFER, INVESTMENT, LOAN, MEDICAL, PERSONAL_TRANSFER, PROPERTY_PURCHASE, PROPERTY_RENTAL, SALARY, SERVICES, TAX, TRAVEL, UTILITY_BILL, and OTHER.

A missing required value returns transfer_purpose_required; a value outside the selected option's allowed set returns transfer_purpose_unsupported. If the quote predates purpose capability metadata, transfer creation returns quote_refresh_required; request a fresh quote and use its requirements. The selected purpose is part of the idempotent transfer instruction, so changing it while reusing an idempotency key is a conflicting replay.

Idempotency ​

Generate one idempotency key for each logical transfer. If the create request times out or the connection drops, retry the same request with the same key. A matching replay returns the existing transfer instead of creating another one.

Do not reuse a key for a different quote option, destination, transfer purpose, or business instruction. Heron rejects conflicting replays.

Destination binding ​

The destination must match the selected quote option. Later edits to the saved account or wallet do not redirect an existing payment.

If the saved destination changed after it was quoted, request a new quote before creating the transfer.

Filtering transfers ​

GET /v1/transfers accepts statuses, an array of status values. Repeat the query parameter to match any of them, for example:

http
GET /v1/transfers?statuses=READY_TO_FUND&statuses=AWAITING_FUNDS

Omit statuses to include all statuses; empty or invalid values are rejected. Keep the same status set while traversing a cursor-paginated list. Changing that set requires restarting pagination. The dashboard's Awaiting Funds filter includes both funding states.

Collection account setup ​

Some providers must activate a receiving account before funding details are available. The transfer remains PENDING_SETUP until the account is active, then becomes READY_TO_FUND. If activation fails or remains pending, contact support. Do not send funds to rejected or inactive account instructions.

Transfer lifecycle ​

Use the exact status value returned by the API:

PhaseStatusesMeaning
SetupPENDING_SETUPHeron is preparing the execution path.
FundingREADY_TO_FUND, AWAITING_FUNDSReady to generate payment details, or provider setup is waiting for payment.
ExecutionIN_PROGRESS, IN_REVIEWThe payment is moving or requires review.
FinalCOMPLETED, FAILED, RETURNED, REFUNDED, CANCELEDThe transfer reached a terminal outcome.

Both funding statuses normally display as “Awaiting Funds” in the customer interface. Expired instructions display as “Payment instructions expired” in red; submitted payments display as “Payment pending confirmation”. READY_TO_FUND means the transfer can generate payment details; it does not guarantee usable details already exist. AWAITING_FUNDS describes the provider setup state; check activeFundingSession and its expiry for usable instructions. The page banner and timeline use the current funding session; a submitted proof on an expired, replaced session does not mean the new instructions have been funded. Expiry and pricing refresh can return a transfer to READY_TO_FUND without making it eligible for cancellation again.

The funding section shows whether you need to generate details, send funds or wait for confirmation. Expired instructions must be renewed before payment.

A RETURNED transfer can subsequently become REFUNDED when the provider confirms refund completion. Continue accepting status updates after a return. In the Heron transfer-detail page, a return with reason due_transfer_refund_submitted displays as “Returned — refund pending” and continues refreshing until refund completion. Other terminal returns stop automatic refreshes.

Use the top-level status for the transfer lifecycle and top-level blockingParty to identify whether the next action is with the CUSTOMER, HERON_OPS, PROVIDER, or no party (NONE).

There is no top-level progressStage on a transfer. Current granular progress is reported on each item in legs[*].progressStage. For multi-leg transfers, present the legs in position order and show each leg's stage rather than combining the values into an undocumented transfer-level stage. Entries in timeline[*].progressStage describe historical transitions, not the current aggregate state. If your UI needs one overall indicator, use the transfer's top-level status and blockingParty. Do not derive it from the number of legs or an arbitrary maximum stage.

Reading the transfer timeline ​

The timeline shows payment milestones, reviews, funding expiry and renewal. Expand an entry for available reason codes, provider details and payment references. Times use your local timezone. A payment submission is not confirmation of settlement; use the transfer's current status.

API timeline entries can include presentation.stage and presentation.reason for customer-facing milestones. For cancellation actor details, read statusHistory[].createdByActorId and the optional, nullable statusHistory[].createdByName on the cancellation status change. Available receipts and references appear in nullable legs[].paymentDetails.receipt and legs[].paymentDetails.reference. The optional refundPending flag distinguishes a pending refund without requiring clients to interpret provider-specific reason codes.

Fund the transfer ​

The transfer detail can expose awaitingFundsInstructions. For workflows that need a dedicated funding session, call:

http
POST /v1/transfers/{transferId}/funding-session

When pricing needs refreshing before funding, Heron refreshes only the transfer’s selected route. The providers, leg order, and rails (including any intermediate blockchain and asset) stay the same for both authenticated funding sessions and public payment links. If that route is unavailable, the pricing refresh fails rather than selecting an alternative route. Existing pricing acceptance and slippage checks still apply.

Use the exact amount, destination coordinates, and reference returned for that transfer. When proof of payment is requested, upload a PDF, JPEG, or PNG of up to 10 MB as multipart/form-data. The binary must use the form field name file:

bash
curl --request POST "$HERON_API_URL/v1/transfers/${TRANSFER_ID}/funding-proof" \
  --header "X-API-Key: ${HERON_API_KEY}" \
  --form "file=@./proof-of-payment.pdf"

Use the same HERON_API_URL environment that created the transfer. Let your HTTP client set the multipart Content-Type header so it includes the required boundary; do not send the file as JSON. A successful upload returns 200 with the accepted funding-proof record. Proof submission does not mean the transfer is complete. Continue tracking its status.

Public payment links can return EVM_TRANSACTION_PLAN instructions for compatible crypto-origin provider-contract transfers, or attach a walletTransactionPlan to ordinary CRYPTO_DEPOSIT instructions while retaining the manual deposit address. Execute plan.steps in position order with one connected wallet on the specified chain. Before opening the wallet confirmation, reserve the next step through:

http
POST /public/payment-links/{token}/funding-session/{fundingSessionId}/wallet-transaction-claim

The claim body must prove control of the connected wallet. Choose an RFC 3339 walletProofExpiresAt after the current time but no later than five minutes from now or the funding session expiry, whichever comes first. Sign this exact UTF-8 message with the connected wallet (signMessage / personal_sign), preserving the blank second line and lowercasing the wallet address:

text
Heron wallet funding authorization

Funding session: {fundingSessionId}
Session version: {fundingSession.updatedAt}
Chain ID: {plan.network.chainId}
Wallet: {walletAddressLowercase}
Expires at: {walletProofExpiresAt}

Send the resulting signature with the step coordinates and reserved nonce (or null when the wallet does not expose one yet):

json
{
  "position": 0,
  "chainId": 781501,
  "walletAddress": "0x...",
  "nonce": 7,
  "walletProofExpiresAt": "2026-09-03T18:30:00.000Z",
  "walletProofSignature": "0x..."
}

The reservation prevents two tabs from broadcasting the same payment step. After the wallet returns a transaction hash, bind that exact hash to the claim through:

http
POST /public/payment-links/{token}/funding-session/{fundingSessionId}/wallet-transaction

Heron verifies the sender, reserved nonce, destination, calldata, value, chain, and receipt against the stored plan. Persist the hash locally before reporting it so a page reload can safely retry the same submission. Submitting the same claim and hash again is idempotent. A transient RPC or provider failure leaves the submission pending for background reconciliation; it does not authorize a second payment. A definitively failed attempt can be replaced by a later attempt for the same step. Do not submit a later step until the preceding step is confirmed.

If a wallet submission is pending or its outcome is uncertain, keep the existing transfer and claim. Do not authorize another payment while waiting for confirmation. A replacement transaction must retain the intended payment call; a wallet cancellation or different call can fail the attempt.

Track updates ​

Use GET /v1/transfers/{transferId} for an on-demand read and customer webhooks for asynchronous updates. Subscribe to both:

  • transfer.status_changed for the normalized transfer state
  • transfer.leg_status_changed for execution-path detail

Webhook events can be duplicated or arrive out of order. Deduplicate by event id and retrieve the current transfer before applying irreversible side effects.

Reconciliation ​

Store the Heron transfer id, your clientReference, amounts, currency or asset, current status, and selected transferPurpose. Use Exports & Reporting for bulk reconciliation and status-history output.

Destination approval ​

Some providers require approval of the receiving bank account or wallet before payment details can be generated. This is separate from identity onboarding. A quote can be available while its destination is not yet approved.

In the Desk, affected quotes show Requires destination approval, or Destination approval pending. There is no notice for providers without this requirement or for a selected destination whose approval is already known.

After creating the transfer, use the destination approval step to authorize sharing that selected account and its beneficiary details with the provider. Adding an account to Heron or activating a provider does not register all your accounts with that provider. Heron records your consent and checks pending registrations in the background, so you can leave the transfer page during review.

API clients can use:

  • GET /v1/transfers/{transferId}/destination-approval with transfers:read to inspect readiness. The response is null when the provider does not require destination approval.
  • POST /v1/transfers/{transferId}/destination-approval with transfers:write and { "consentToShare": true } to register the transfer's saved destination. For providers requiring wallet ownership declarations, also send isSelfHostedWallet: true and confirmBeneficiaryOwnsWallet: true only when both statements are accurate.

Possible states are NOT_REGISTERED, SUBMITTING, PENDING, APPROVED, REJECTED and ACTION_REQUIRED. While registration is submitting or pending, wait and re-read readiness instead of repeatedly registering. Payment details are unavailable until approval is confirmed.

If canResubmit is true, obtain fresh consent before repeating the registration POST. Rejected destinations with incorrect details require correction. Editing a saved destination or switching the linked provider account can require a new approval.

For a hosted or third-party wallet, send both ownership flags as false and include a beneficiary with entityType: "INDIVIDUAL", firstName, lastName, or entityType: "BUSINESS", businessName. Its address requires street, ISO alpha-2 country, and at least one of buildingNumber or buildingName; city is optional. The approval response's requiresBeneficiaryDetails identifies saved third-party destinations. Heron shares these details only after consent for the selected transfer; they are not included in approval or quote responses.

Quotes between transfer legs ​

For supported providers, including Due, Heron refreshes an expired quote before starting a later leg, using the amount delivered by the preceding leg. The transfer policy’s slippage limit still applies. A rate outside that limit places the transfer in review for Heron operations; completed legs are not repeated.

Payment reference / memo ​

A quote may include paymentMemoRequirement with required, minLength, and maxLength. Supply paymentMemo when required; it is separate from the internal clientReference and may be shared with the payment provider and recipient. Due memos accept letters, numbers, spaces, and & - . /. A single character is allowed; repeated copies such as AAA are rejected. CAD EFT supports 1–16 characters when supplied. Its business-to-business payments require an invoice or other document reference number; transfers involving an individual follow the general optional rule. Older CAD quotes without memo requirements must be regenerated before creating a transfer. Requirements are checked before transfer creation and again before Due submission, including when Due follows an earlier funding leg. Quote refresh preserves the transfer's memo.

Cancel an unused transfer ​

You can cancel an unused transfer from its detail page in Desk or through the API:

http
POST /v1/transfers/{transferId}/cancel
Content-Type: application/json

{"note":"Duplicate invoice"}

The request body may be {}. The optional note must contain 1–500 characters after trimming. Cancellation requires the transfers feature and transfers:write permission (or organization admin access).

Read cancellation.eligible and cancellation.reason on the transfer detail response before showing a cancellation action. Heron checks eligibility again when you submit:

  • The transfer must be READY_TO_FUND, with every leg and progress stage still PENDING_SETUP.
  • No funding session may have been created, even if it has expired. No funding instructions, provider execution, received funds, transaction submissions, or execution claims may exist.
  • Transfers with a previous setup attempt or ambiguous execution history cannot be canceled. Expired or released claims do not prove that the provider never received the request.
  • If eligibility cannot be established, contact support.

A successful request returns 200 and the updated transfer. The transfer and all its legs are CANCELED, and their payment links are revoked. Subsequent funding and execution requests cannot use those links or restart the transfer. History and saved destination references remain available.

Repeating cancellation for an already canceled transfer returns 200 without another history entry, webhook, or notification. No separate idempotency key is needed. The original actor and cancellation note are retained even if a repeated request supplies a different note. Cancellation Cancellation emits the normal transfer.status_changed and leg status webhooks.

If eligibility changes while the confirmation is open, the API returns 409 with code: "invalid_request" and a machine-readable details.reason. Refresh the transfer before trying another action:

ReasonMeaning
transfer_not_ready_to_fundThe current transfer status does not allow cancellation.
legacy_execution_history_unknownEarlier setup history cannot be verified.
setup_attemptedFunding or execution setup has already been attempted.
funding_or_execution_recordedFunding sessions, proofs, or provider events exist.
legs_not_pristineA leg or its progress stage has left initial setup, or legs are missing.
execution_claim_presentA claim exists, including a partial or expired claim.
execution_evidence_presentInstructions, provider results, funds, or transaction records exist.
execution_history_presentEarlier status/progress history prevents cancellation.
transfer_changedThe route changed during submission; refresh and recheck eligibility.

The detail response uses already_canceled for an already canceled transfer. Missing transfers, including those outside your organization, return 404. Invalid notes return 400; insufficient permissions return 403.

Cancellation cannot be undone and is not a refund. AWAITING_FUNDS and later transfers are ineligible because a payer may already have sent money, and issued bank instructions can remain usable. Contact support about those transfers rather than attempting cancellation.

Delayed transfer review ​

transfer_leg_progress_stalled means Heron has escalated an in-progress leg for operations review because it has not advanced beyond its expected processing window. It is not a provider rejection or a final payment outcome.

The customer timeline labels this “Taking longer than expected” and explains that Heron has been notified and is monitoring the transfer. After the transfer progresses, the delay uses past-tense wording. Technical reason codes remain in the step’s expanded details.

Heron continues checking the provider while this review is active. Routine processing updates keep the review open; a completion received by webhook or polling updates the transfer to COMPLETED and clears the stall reason. Other provider outcomes are processed normally.

This review is an operational alert, not a promised delivery deadline. Contact support when you need an update; do not create a replacement transfer because a payment is delayed.

Receiving-account setup in the timeline ​

When a transfer is waiting for a provider-managed receiving account, the timeline shows Setting up your receiving account with the provider and source currency. This is a current-state entry marked Now, not a dated provider event. Routine checks do not create additional entries, and pending setup does not imply that a provider review has been confirmed. The entry disappears when setup is no longer pending. Once the account is active and the transfer becomes ready to fund, the readiness update says Receiving account ready and explains the next funding step.

Transfer-detail legs expose an optional, nullable receivingAccount object with its current status (PENDING, ACTIVE, FAILED, or DEACTIVATED). A null value means no managed receiving account is linked. This field contains no bank details or provider account identifiers and does not represent account status history.

Receiving-account activation is checked automatically. If setup cannot be completed, the transfer requires review. Contact support rather than starting a duplicate payment.

Expired payment instructions ​

Transfer summaries and public payment-link responses include an optional funding summary: status is the funding-session status (reported as EXPIRED once unused instructions expire), expiresAt is its expiry timestamp (or null), and paymentSubmitted indicates submitted proof of payment or a pending/confirmed funding transaction. It contains no bank details or provider identifiers. The underlying transfer status is unchanged.

For transfers awaiting customer funding, the dashboard and payment link show Payment instructions expired in red when the session is expired or its expiry time has passed. Do not send funds using previous instructions. Choose Generate new payment details and use the details currently shown. A provider may revalidate the existing transfer and return the same details with a renewed validity window. Generating new payment details may retain the transfer's existing amounts and pricing. Check the details shown before sending funds.

If payment was already submitted, the UI shows Payment pending confirmation and does not prompt another payment. Wait for confirmation rather than sending funds again.

The API timeline includes PAYMENT_DETAILS_EXPIRED and PAYMENT_DETAILS_RENEWED events where available. Renewing details does not mean a new payment is required if payment is already pending.

Released under the Commercial License.