InOut

Webhooks

Operation status notifications, delivery retries, and event deduplication.

When an operation enters a subscribed status, InOut POSTs the operation snapshot to the callback URL configured for the shop that owns the operation. Use /v1/get_operation_state to reconcile after a missed or ambiguous notification.

When a callback fires

By default (an empty callback-status filter) a callback is sent for the terminal statuses completed, failed, rejected, and cancelled, plus the intermediate statuses awaiting_confirmation, pending_confirmation, and manual_check. A shop may configure a strict callback-status filter; when it is non-empty, only listed statuses are delivered — this applies to the intermediate statuses too, so a shop that narrows the filter must list awaiting_confirmation / pending_confirmation / manual_check explicitly to keep receiving them. If no provider route accepts the operation, it is recorded as rejected and the callback is delivered only when that status passes the shop filter.

A callback is sent when a payment enters awaiting_confirmation — this is an intermediate status (not terminal) that fires early so you can prompt the end user to upload a receipt. The terminal callback still fires later once the receipt is verified and the payment reaches a terminal status. Treat the awaiting_confirmation callback as an early, best-effort notification. It is attempted once and does not use the terminal retry schedule.

A callback is also sent when an uploaded receipt enters pending_confirmation. This means the receipt was accepted and automatic validation is in progress; do not ask the end user to upload it again. It is a one-shot, best-effort intermediate callback and does not replace the eventual terminal callback.

A callback is also sent when a payment enters manual_check — an intermediate status (not terminal) that fires when an uploaded receipt is flagged for manual review (for example, our antifraud found the receipt suspicious). The payment is still in flight; you may surface it as "under review" and, if you automate around it, react to the flag (e.g. alert an operator). A terminal callback (completed / failed) still fires later once review resolves. Like the other receipt states, it is a best-effort intermediate: attempted once, it does not use the terminal retry schedule, and it is subject to the shop callback-status filter (delivered under the default empty filter; otherwise opt in by listing manual_check). If your integration does not recognise the value, ignore it — it never replaces the terminal callback. Receipt replacement rules are described in Confirm payment.

Every manual_check snapshot includes manual_check_reason. Its values are potential_fake (the receipt may be forged or otherwise suspicious) and not_terminal (the receipt does not prove a completed transfer: the shown transfer is still in progress/non-terminal, the uploaded file is not a PDF and could not be validated, or automatic validation was temporarily unavailable). The field is omitted for all other statuses. If the reason is unknown, the API returns potential_fake.

A callback is not sent for pending — by design.

Configuration

The callback URL is configured per shop in the admin UI (Shops → edit shop → Callback URL). It must be:

  • An absolute https:// URL (or http:// for testing).
  • Reachable by InOut and able to respond within 10 seconds.

If the callback URL is empty for a shop, no webhook is delivered. The operation still reaches its terminal status; the merchant is expected to read state via /v1/get_operation_state.

Request

POST <callback_url>
Content-Type: application/json

Body — a public snapshot of the operation:

{
  "operation_id": "f6c2e7d4-3e5b-4f1f-8a02-9d6c7e8a1b22",
  "event_seq": 3,
  "type": "payment",
  "status": "completed",
  "method": "WT_RUB_PHONE",
  "expected_amount": "1500.00",
  "actual_amount": "1500.00",
  "currency": "RUB",
  "actual_currency": "RUB",
  "settlement_amount": "1470.00",
  "settlement_currency": "RUB",
  "commission_amount": "30.00",
  "payout_destination": {
    "phone": {
      "phone_number": "+79991234567",
      "name": "Ivan Ivanov",
      "bank": "sber"
    }
  }
}

payout_destination is a typed sum: exactly one of phone or c2c is set (the variant lines up with Method), or the field is null if InOut has not yet allocated a destination. The card variant looks like {"c2c": {"card_number": "4111…", "name": "…"}}. actual_currency is the currency actual_amount is denominated in (the source currency you sent). fx_rate/fx_source (the conversion quote) are present as soon as a route with auto-conversion is chosen — even on the early awaiting_confirmation callback, before money has actually moved. recorded_currency/recorded_amount are the ledger FACT (actual amount × rate) and are gated on the operation actually having a non-zero actual_amount — they are absent on failed/rejected/cancelled callbacks (no money moved) and present on completed. recorded_amount is additionally exposed as a minor-units pair (recorded_amount_minor + recorded_amount_scale) for integrators that account in minor units, following the same gate.

external_confirmation is present only while status is awaiting_confirmation. It describes the action currently required from the merchant and is omitted from every other intermediate or terminal callback, including pending_confirmation, manual_check, and cancelled.

settlement_amount is the amount that actually moved on your balance, net of the InOut commission, denominated in settlement_currency (the ledger currency — equal to currency when no conversion happened). For a payment it is the amount credited to you after commission (gross − commission_amount); for a payout it is the total charged to you including commission (gross + commission_amount). commission_amount is the InOut commission for the operation, in the same currency. On a partial capture both figures reflect the captured amount.

Use operation_id to match the callback against the create response and to call /v1/get_operation_state if a follow-up reconciliation is needed.

Acknowledging

Reply with any 2xx status (typically 200 OK with an empty body). Anything outside the 200–299 range — including network errors and timeouts — counts as a failure. Terminal events and manual resends retry according to the schedule below; automatic intermediate receipt notifications are attempted once. Redirects are not followed: a 3xx reply is a failed attempt, so configure the final callback URL directly.

Retries

Terminal callbacks and manual resends follow a configurable back-off schedule. Events for one operation are delivered in order: an event waiting for a retry blocks later events for that operation until it succeeds or exhausts its attempts. Different operations can be delivered concurrently. The first attempt starts when the event reaches the front of its queue; on failure, InOut waits the next gap and retries.

The table below is the default when the shop has no custom callback retry intervals. A configured shop schedule replaces these gaps.

AttemptWait before this attempt
1 (initial)—
230 seconds
31 minute
45 minutes
530 minutes
61 hour
73 hours
86 hours
912 hours
10 (final)24 hours

With the default schedule, after the 10th attempt the auto-retry loop stops. The terminal operation state is still queryable via /v1/get_operation_state, and an operator can enqueue a manual resend from the admin UI. A manual resend is a new snapshot event with a new event_seq, in the same operation queue. It does not overtake or cancel an earlier event's retry. Delivery history is available to operators in the admin UI.

Each event keeps the snapshot captured when it was queued. Automatic retries send the same body and event_seq, even if the operation has since changed.

The per-attempt HTTP timeout is 10 seconds. Make sure your handler can respond inside that window — do the heavy lifting asynchronously.

Idempotency

The same operation can produce multiple delivery attempts, and an authoritative admin/provider reversal can move one operation through the same status more than once (completed → failed → completed). event_seq is a monotonically increasing event number within one operation. It is not a global sequence, and clients must not require consecutive numbers.

  • Serialize processing per operation_id. In one transaction, compare event_seq with the last applied sequence, update the snapshot and its business effects, and save the new sequence.
  • Acknowledge an equal or lower sequence with 2xx without applying it again.
  • Do not deduplicate permanently by (operation_id, status): the same status can recur with a higher sequence.
  • A higher sequence can also be a manual resend of the same state. Apply the difference from your stored snapshot, not another unconditional credit/debit.
  • If event_seq is missing, reconcile the operation with /v1/get_operation_state instead of assigning a guessed sequence. Do not reset your last stored sequence during reconciliation.

After ambiguity or an apparent reversal, reconcile the current operation state before applying irreversible external side effects.

Handler examples

Node.js / Express

app.post("/payments/webhook", express.json(), async (req, res) => {
  const op = req.body;
  if (op.type !== "payment" && op.type !== "payout") {
    return res.sendStatus(200);
  }

  if (op.event_seq == null) {
    await reconcileOperation(op.operation_id);
  } else {
    // Application helper: atomically ignore event_seq <= last applied,
    // apply the snapshot difference, and save event_seq for this operation.
    await applySnapshotIfNewer(op.operation_id, op.event_seq, op);
  }
  res.sendStatus(200);
});

Python / FastAPI

@app.post("/payments/webhook")
async def payment_webhook(op: dict):
    if op.get("event_seq") is None:
        await reconcile_operation(op["operation_id"])
    else:
        # Application helper: compare sequence, apply snapshot differences,
        # and store the sequence in one transaction per operation.
        await apply_snapshot_if_newer(op["operation_id"], op["event_seq"], op)
    return Response(status_code=200)

The helper functions above are application code you implement with your own database and authenticated API client. Commit the snapshot and sequence before acknowledging; return an error if processing fails so terminal events can retry. Keep processing under the 10-second timeout and enqueue slower work from the same transaction.

Verifying the sender

InOut does not sign callback bodies. To verify the sender, restrict the receiving endpoint to the InOut sending IP addresses or use a shared-secret header agreed with your account manager.

On this page