Skip to main content

Receive transfers without orders

Use this flow when your system already has order codes and you only need to know when money for them arrives, for example a static QR code at a shop counter. You do not create a payment per order. Ha Tang reports every transfer into the account as transaction.received, with your order code if it found one in the description.

Before you start​

  • An active bank connection.
  • A webhook endpoint subscribed to transaction.received. This flow has no IPN, because there is no payment to attach an ipn_url to.

1. Tell Ha Tang what your order codes look like​

Set payment_code_pattern on the bank account: a prefix and a length range. Ha Tang then looks for it in every transfer description.

curl -X PATCH https://api.hatang.dev/payments/bank-connections/bcn_123 \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "config": { "payment_code_pattern": { "prefix": "DH", "min_length": 4, "max_length": 8, "charset": "digits" } } }'

How the code is found​

  1. The description is normalized: accents removed, đ becomes D, everything upper-cased, and every character that is not a letter or a digit dropped. ck dh 2025-0919 cam on becomes CKDH20250919CAMON.
  2. Ha Tang looks for the prefix followed by min_length to max_length characters of the charset, and takes the first match, as long as possible.

With the pattern above, DH20250919 is found in ck dh 2025-0919 cam on.

caution

Spaces are removed before the search, so with charset: "alnum" the words after your code run into it: DH1001 cam on becomes DH1001CAMON and the match is DH1001CAMO. Prefer digits, or make every code exactly max_length long.

2. Show a VietQR with your code​

GET /payments/qr builds a VietQR for any amount and description. Nothing is stored.

# JSON with the EMVCo payload
curl "https://api.hatang.dev/payments/qr?amount=99000&content=DH20250919" \
-H "Authorization: Bearer $HATANG_API_KEY"

# An SVG image
curl "https://api.hatang.dev/payments/qr?amount=99000&content=DH20250919&format=svg" \
-H "Authorization: Bearer $HATANG_API_KEY" -o qr.svg
{
"object": "vietqr",
"payload": "00020101021238530010A0000007270123000697042201099104526580208QRIBFTTA53037045405990005802VN62140810DH202509196304FCC9",
"connection_id": "bcn_01a0b90428fd7c499145a12c133abe41",
"bank": { "bin": "970422", "account_number": "910452658", "account_name": "NGUYEN VAN A" },
"amount": 99000,
"content": "DH20250919"
}
ParameterMeaning
amountVND. Leave it out for a QR where the payer types the amount (a counter sticker)
contentThe description to prefill, up to 50 characters
connection_idWhich account; required only when several are linked
formatjson (default) or svg
logoWith format=svg: 0 leaves the Ha Tang mark out of the image

The QR only prefills the description. A payer who transfers by hand must type the code themselves, so print it next to the QR.

3. Receive transaction.received​

{
"type": "transaction.received",
"created_at": "2026-09-19T09:34:12.946Z",
"data": {
"object": "transaction",
"id": "txn_01a0b90429dc79c7a5689c7a0c485a01",
"environment": "production",
"connection_id": "bcn_01a0b90428fd7c499145a12c133abe41",
"direction": "in",
"amount": 99000,
"currency": "VND",
"description": "DH20250919 chuyen tien",
"code": "DH20250919",
"merchant_code": "DH20250919",
"reference": "FT1789810452930810",
"counterparty": { "account": "0987654321", "name": "NGUYEN VAN KHACH", "bank": "970436" },
"booked_at": "2026-09-19T09:34:12.930Z",
"source": "push",
"payment_id": null,
"match_status": "unmatched",
"invoice_exempt": null,
"created_at": "2026-09-19T09:34:12.946Z"
}
}
FieldMeaning
merchant_codeYour order code as found in the description; null when none was found
codeThe code stored on the transaction: a Ha Tang payment code when the transfer paid a payment, otherwise your order code
referenceThe bank's reference number
sourceHow Ha Tang learned about it: push (the bank notified it), reconcile (read from the account history), import (a statement) or simulator (development)
payment_id, match_statusSet when the transfer paid one of your payments (matched); unmatched otherwise
counterparty.bankThe payer's bank BIN, when the bank reports it

Your handler should:

  1. Verify the webhook signature (see Webhooks).
  2. Find the order by merchant_code, and compare amount with what it should be.
  3. Store data.id. The same transaction is never reported twice, but your endpoint may receive the same delivery again after a timeout.
  4. Check environment and direction.

Edge cases​

SituationWhat you get
The payer forgot the codemerchant_code: null. Match by amount and counterparty.name, or ask the payer
The payer sent the wrong amountThe amount as transferred; deciding is up to you
The payer paid twiceTwo transactions with different id and reference
The bank pushed the same transfer twiceOne transaction (same reference and amount on the account)
The transfer also carries a payment codeIt settles that payment (payment.succeeded, etc.) and transaction.received has payment_id set. A transfer is never counted twice

Filter what is reported​

Per bank account, under transaction_events in the config or in the dashboard:

SettingDefaultEffect
directioninall also reports money going out
require_codefalsetrue reports only transfers where a code was found or a payment matched
min_amount0Transfers below this amount are not reported

List and look up transactions​

Every transfer is stored, whether or not it was reported.

curl "https://api.hatang.dev/payments/transactions?direction=in&match_status=unmatched&limit=50" \
-H "Authorization: Bearer $HATANG_API_KEY"
ParameterMeaning
limit1 to 100, default 20
afterThe next_cursor of the previous page
connection_idOne bank account
directionin or out
match_statusmatched, unmatched or ignored
payment_idThe transactions that settled this payment

The answer is { "object": "list", "data": [...], "has_more": true, "next_cursor": "txn_..." }, newest first. GET /payments/transactions/{id} returns one transaction.

Next steps​

View this page as Markdown