Skip to content

Short transfers: accept actual value automatically ​

When authoritative evidence confirms 0<V<X, all five non-amount checks pass, and there is no ambiguity or policy hold, the system settles automatically at A=V, partial:true, settled_via:'auto' — no review and no force-complete call.

Example: an order for THB 5,000.00 where the user transferred THB 3,000.00 (wrong entry, or not enough balance), and the slip verifies that THB 3,000.00 really came from the declared account.

POST /v1/pay/deposits/{id}/force-complete is a compatibility endpoint only for a short-pay that is already under_review because of another non-amount, ambiguity, or policy reason. After support resolves that other reason, it can accept A=V with settled_via:'review'.

The compatibility endpoint is not an approval

force-complete asks a reviewer to decide. It is not an instruction to credit. The response is 202 with status: "under_review". No money moves until the deposit.settled webhook arrives.

Code that says "call force-complete, then credit the user" credits goods you have not been paid for. If the clean short-pay already closed automatically, this call returns 409 deposit_not_reviewable; read the latest order and credit amount_received_minor.


When to use it ​

SituationUse this endpoint?
The user only underpaid and all five non-amount checks pass❌ No — the system settles automatically at A=V<X, partial:true, settled_via:'auto'
The user underpaid and the order is under_review for another sender/time/receiver/ambiguity/policy reason✅ Yes, after attaching evidence; support must resolve the other reason before approval
User overpaid❌ No — if every other verification dimension passes, the order settles immediately at the ordered amount and carries an overpayment{claim_id, amount_minor, action_required:'contact_support'} flag; the excess is neither credited nor refunded automatically
User transferred twice for one order❌ No — the original order settles normally and the excess arrives separately as deposit.overpaid 🔴 do not touch the original order
No slip at all yet❌ Not possible — at least one verified slip is required first
User has not transferred and you want to close the order❌ Use cancel instead

Request ​

bash
curl -X POST https://api.example-wl.com/v1/pay/deposits/dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B/force-complete \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "PG-Timestamp: 1755750240" \
  -H "PG-Nonce: 01JQA5TE0R2T4V6X8Z0B2D4F6H" \
  -H "PG-Version: 2026-08-01" \
  -H "Idempotency-Key: force-ORD-9981-01" \
  -H "PG-Signature: v1=6b21...ff84" \
  -H "Content-Type: application/json" \
  -d '{
    "verified_amount_minor": 300000,
    "note": "รายการติดตรวจชื่อผู้โอน ยืนยันยอดจริงจากสลิป 3,000 บาท และแนบหลักฐานให้ตรวจชื่อแล้ว"
  }'

The note above says the order is in sender-name review, gives the actual amount, and points to the extra evidence. Reviewers are Thai-speaking, so write the note in Thai.

fieldtyperequiredconstraints
verified_amount_minorinteger✅The merchant's amount claim for review, in satang · not authoritative; the system derives V again from evidence and requires 0<V<X
notestring✅🔴 at least 20 characters; anything shorter returns 422 · write it so someone with no context understands

A short note is rejected at the database level

"ok" · "โอนขาด" (underpaid) · "ลูกค้าแจ้ง" (customer told us) — all too short. Write the full picture: what other review reason exists, how much was transferred, and what the evidence is. A reviewer decides based on this text.

Response ​

json
{
  "id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
  "object": "deposit_order",
  "version": 6,
  "status": "under_review",
  "is_terminal": false,
  "merchant_reference": "ORD-9981",
  "amount_minor": 500000,
  "currency": "THB",
  "review": {
    "opened_at": "2026-08-21T04:24:10.552Z",
    "escalated": false,
    "four_eyes_required": false,
    "estimated_resolution_bucket": "under_1h",
    "reasons": [
      {
        "code": "SENDER_NAME_MISMATCH",
        "display_th": "เราได้รับสลิปของคุณแล้ว ชื่อผู้โอนไม่ตรงกับที่แจ้งไว้ เจ้าหน้าที่กำลังตรวจสอบ เงินของคุณไม่หาย ไม่ต้องโอนซ้ำ",
        "display_en": "We received your slip. The sender name needs review. Your money is safe; do not transfer again.",
        "actionable_by": "support",
        "detail": { "registered_name_masked": "ส*** ใ***" }
      },
      {
        "code": "AMOUNT_MISMATCH",
        "display_th": "เราได้รับสลิปของคุณแล้ว ยอดที่โอนไม่ตรงกับยอดของรายการนี้ เจ้าหน้าที่กำลังตรวจสอบ เงินของคุณไม่หาย ไม่ต้องโอนซ้ำ",
        "display_en": "We received your slip. The transferred amount does not match this order. Our team is reviewing it.",
        "actionable_by": "end_user",
        "detail": { "expected_minor": 500000, "observed_minor": 300000 }
      }
    ]
  },
  "metadata": { "campaign": "aug-cashback", "order_id": "ORD-9981" }
}

Conditions that must hold ​

#ConditionIf it fails
1The order is under_review for another real non-amount, ambiguity, or policy reason409 deposit_not_reviewable; a clean short-pay that already closed never re-enters review
2Authoritative verified-slip or poll evidence existsThe request does not go through — upload a slip first
3note is ≥ 20 characters422
4The reported verified_amount_minor is smaller than the order's amount_minor422 validation_error; this field is still not authoritative
5At approval time, the system derives V from evidence rather than trusting the request and requires 0 < V < X, where X is the original reservationThe review cannot be approved if the evidence does not satisfy the guard

How the shortfall returns to liquidity

Approval performs one atomic update: it releases the original reservation X, fills the withdrawal by the verified amount V, and returns X−V to that withdrawal's remaining amount for later matching. Journals and fees use V only — force-complete does not park the shortfall in SUSPENSE or open a refund path. If X−V becomes dust, the W11 policy and cap handle it later, or the normal cancel/expiry path releases it.


Effect on the credited amounts ​

This is a reviewed short-pay example after support resolved the other non-amount, ambiguity, or policy reason. A clean short-pay sends the same financial fields with settled_via:"auto" and previous_attributes.status:"slip_processing".

json
{
  "id": "evt_01K3MC4T7V9XB2DQF0KHRZ6N8M",
  "object": "event",
  "type": "deposit.settled",
  "api_version": "2026-08-01",
  "livemode": true,
  "created_at": "2026-08-21T04:52:41.318Z",
  "sequence": 88296,
  "account": { "merchant_id": "mch_01K2G4H9RT5VQ0", "white_label_id": "wl_01K1QZ2MNB4XD7" },
  "data": {
    "object": {
      "id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
      "object": "deposit_order",
      "version": 8,
      "status": "settled",
      "is_terminal": false,
      "merchant_reference": "ORD-9981",
      "amount_minor": 500000,
      "ordered_amount_minor": 500000,
      "partial": true,
      "settled_via": "review",
      "liquidity_source": "internal",
      "currency": "THB",
      "transferred_amount_minor": 300000,
      "amount_received_minor": 300000,
      "fee_total_minor": 5300,
      "fee_breakdown": [
        { "component": "processing_fee", "amount_minor": 3000, "rate_bps": 100 },
        { "component": "fixed_fee", "amount_minor": 2000 },
        { "component": "vat", "amount_minor": 300 }
      ],
      "credited_amount_minor": 294700,
      "overpayment": null,
      "balance_transaction_id": "btxn_01K3MC4T6R8WA1CPE9JGQY5M7L",
      "settled_at": "2026-08-21T04:52:41.104Z",
      "clearing_available_at": "2026-08-24T04:52:41.104Z",
      "metadata": { "campaign": "aug-cashback", "order_id": "ORD-9981" }
    },
    "previous_attributes": { "version": 7, "status": "under_review" }
  }
}
fieldValue hereMeaning
partialtrue🔴 the flag that says this order was settled at the amount received, not the full amount; it applies to automatic and reviewed short-pay
ordered_amount_minor500000The amount ordered at creation time
amount_received_minor300000The amount to credit the end user
fee_total_minor5300The fee is calculated on what actually arrived, not on the ordered amount
credited_amount_minor294700What lands in your balance = 300000 − 5300

Which number credits whom

The end user gets amount_received_minor = THB 3,000.00. Your own balance moves by credited_amount_minor = THB 2,947.00.

🔴 Never credit the user with ordered_amount_minor — that is money they did not transfer.

js
// Node — a handler that covers both normal and partial settlements
async function onDepositSettled(evt) {
  const d = evt.data.object

  await db.tx(async (t) => {
    const { rows: [correlation] } = await t.query(
      `SELECT our_order_id, our_user_id
         FROM pg_deposit_correlations
        WHERE pg_deposit_id = $1`, [d.id])
    if (!correlation) throw new Error('missing deposit correlation; retry webhook')

    // This helper is the UPSERT + `WHERE stored_version < incoming_version` from webhook ordering.
    const applied = await db.applyDepositSnapshotIfNewer(t, d, correlation)
    if (!applied) {
      await db.markWebhookApplied(t, evt.id)
      return
    }

    // Every helper receives transaction `t`: version + A credit + wallet mirror + outbox commit together.
    const credited = await db.creditUserOnce(
      t, correlation.our_user_id, d.id, d.amount_received_minor)
    if (credited) {
      await db.applyMerchantBalanceOnce(
        t, d.id, d.credited_amount_minor, d.balance_transaction_id)
      await enqueueEffect(t, `deposit:${d.id}`, 'release_goods', {
        order_id: correlation.our_order_id,
        received_minor: d.amount_received_minor,
        ordered_minor: d.ordered_amount_minor
      })
      if (d.partial) await enqueueEffect(t, `deposit:${d.id}`, 'notify_actual_amount', {
        user_id: correlation.our_user_id,
        received_minor: d.amount_received_minor,
        ordered_minor: d.ordered_amount_minor,
        shortfall_minor: d.ordered_amount_minor - d.amount_received_minor
      })
    }
    await db.markWebhookApplied(t, evt.id)
  })
}

Persist pg_deposit_correlations from the create request + response first, as shown in Webhook ordering, and always use that mapping to find the user/order—metadata is optional. Never split applyIfNewer() from the credit/outbox transaction: if the process dies after committing the version but before crediting, the retry is discarded by the version guard and the user never receives the accepted amount.


What the user should see ​

StepCopy (Thai)English gloss
Clean short-pay (settled + partial:true + settled_via:'auto')ได้รับยอดที่โอนจริง 3,000.00 บาท จากรายการ 5,000.00 บาทWe received the actual transfer of THB 3,000.00 against an order for THB 5,000.00.
Reviewed short-pay after compatibility force-complete (under_review)Use review.reasons[0].display_th for the other reason that opened reviewNever claim that the short amount alone is waiting for a person.
Reviewed short-pay approved (settled + partial:true + settled_via:'review')ได้รับยอดที่โอนจริง 3,000.00 บาท จากรายการ 5,000.00 บาทWe received the actual transfer of THB 3,000.00 against an order for THB 5,000.00.
Offer the next stepส่วนที่เหลือ 2,000.00 บาท สร้างรายการใหม่ได้เลยFor the remaining THB 2,000.00, you can create a new order.

202 exists only on the compatibility path; never call it approved

It only means the request for an order with another review reason is queued. A clean short-pay never takes this response path—wait for deposit.settled, then show the accepted amount immediately.


Adding evidence while the review is still open ​

If the order sits in under_review and the user sends more information, keep the document in the merchant's secure system and attach only its reference to the existing case. Do not create a new order.

bash
curl -X POST https://api.example-wl.com/v1/pay/deposits/dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B/evidence \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "PG-Timestamp: 1755750412" -H "PG-Nonce: 01JQA5TG7X9M4TB0YR6WCZ8V2N" \
  -H "PG-Version: 2026-08-01" -H "Idempotency-Key: evidence-ORD-9981-01" \
  -H "PG-Signature: v1=8c41...d97a" -H "Content-Type: application/json" \
  -d '{"kind":"bank_statement_line","reference":"merchant-ledger-2026-08-21#line-004921","note":"Merchant-verified bank-statement line"}'
fieldTypeRequiredNotes
kindstring✅Closed enum: bank_statement_line · chat_transcript · id_document_ref · other
referencestring✅Reference in the merchant's evidence store · id_document_ref is not an identity-card image
notestring✅Free text for the reviewer to read

🔴 This endpoint accepts application/json only: no multipart, file, base64, or directly accessible document URL.

Answers 202 with the full deposit object.

Attaching evidence does not restart the clock and does not change the status

The order stays in under_review. version may move, status does not ⇒ do not show "resubmitted" on screen, and do not create a fresh POST /v1/pay/deposits because you assumed the old one was stuck.

Once the late-slip window has closed, this is not the way out

If the order is already closed (you got 410 late_slip_window_closed on the slip upload), the case goes to claims, not here — this endpoint only attaches to a review that is still open.


If the reviewer does not approve ​

The order moves to rejected with rejection.display_th ⇒ show that text verbatim, along with a copyable deposit_id so the user can contact support.

🔴 rejected does not mean the money was automatically refunded. If real money did arrive, the refund runs on a different path from this status. Never write code that auto-refunds when it sees rejected.


Next ​

เอกสารชุดนี้อิงสัญญา API เวอร์ชัน 2026-08-01