> ## Documentation Index
> Fetch the complete documentation index at: https://docs.onstacks.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Partner funding reference

> Server verification and funding requirements for delegated widget payments.

> [!TIP]
>
> Start with [Add the widget](/ramps/widget/quickstart) for setup, the callback payload and funding submission. Use this page when implementing your server verification.

## Server contract

Your frontend's `fundOrderOnce` helper coordinates your backend and connected wallet. It is not an SDK method. Keep verification, reservation, storage and public API submission on your backend; run connected-wallet calls in your frontend. Together they must:

1. Authenticate the order owner and verify the complete instruction's signature, expiry, widget/workspace/environment, merchant/customer identity, sender, requested amount, asset/network and beneficiary against the stored order. If bank details are collected only in the widget, bind the signed beneficiary hash on first acceptance.
2. Atomically reserve the order and `paymentRef`/`nonce`. Resume a recorded hash; return a waiting state if a transfer is running or its result is unknown. Never approve a second broadcast for the same operation.
3. Complete/resume any swap, then transfer the signed funding amount. Preserve the wallet operation identity for recovery. Durably save the real hash and enqueue submission to Onstacks before acknowledging the widget.
4. Submit and track from the server even after the widget closes. Retry a submission with the same hash; reconcile an unknown broadcast before another transfer.

## Wallet cancellation and uncertain outcomes

Your frontend calls `reply.rejected({ reason: "Wallet request cancelled" })` only after the wallet confirms no broadcast and your backend clears the reservation. Return `{ outcome: "cancelled_before_broadcast" }` from `fundOrderOnce` for this case. The SDK then permits the widget's retry callback for the same instruction. A wallet error code alone is not proof if an earlier broadcast may have succeeded.

For a timeout, unknown broadcast or failed hash recording, retain the reservation and recover the existing transaction. Do not call `reply.rejected`, resend tokens or repeat a completed swap.

## Verify the signature

Use the environment's widget signing secret. This helper checks the HMAC; it does not replace the order, expiry or duplicate checks above. V2 uses the fields below; V3 appends `requestedAmount` and `currency`.

Create `services/verifyOnstacksTransfer.js` in your backend. Keep the signing secret there.

```js services/verifyOnstacksTransfer.js · Backend theme={"system"}
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyTransferSignature(i, secret) {
  if (![2, 3].includes(i.version) || !/^[0-9a-f]{64}$/.test(i.signature ?? ""))
    return false;
  const keys = [
    "version",
    "tenantId",
    "workspaceId",
    "environment",
    "merchantRef",
    "paymentRef",
    "customerId",
    "organizationId",
    "address",
    "sender",
    "amountRaw",
    "amountUnits",
    "token",
    "tokenAddress",
    "decimals",
    "chain",
    "chainId",
    "provider",
    "beneficiaryHash",
    "nonce",
    "submissionToken",
    "issuedAt",
    "validUntil",
    ...(i.version === 3 ? ["requestedAmount", "currency"] : []),
  ];
  const parts = [];
  for (const key of keys) {
    if (i[key] == null) return false;
    let value = String(i[key]);
    if (/[|\r\n]/.test(value)) return false;
    if (["address", "sender", "tokenAddress"].includes(key))
      value = value.toLowerCase();
    parts.push(value);
  }
  const expected = createHmac("sha256", secret)
    .update(parts.join("|"))
    .digest();
  return timingSafeEqual(expected, Buffer.from(i.signature, "hex"));
}
```

Preserve decimal amount strings exactly. The beneficiary hash is SHA256 of `JSON.stringify([bankCode, accountNumber])`, preserving leading zeros. Reject unknown versions and expired instructions. HMAC verification alone does not establish order ownership or prevent duplicate transfers.

## Submit funding and track payout

Your backend submission worker sends this request to the public API. Set `environment` to the verified instruction’s `sandbox` or `live`:

```http Submit funding · Backend theme={"system"}
POST https://api.onstacks.io/v1/ramp/widget/delegated-funding/{paymentRef}?environment=sandbox
Authorization: Bearer {submissionToken}
Content-Type: application/json

{"txHash":"0x...actual-broadcast-hash"}
```

GET the same URL with the same Authorization header to read status. Merchants use only `https://api.onstacks.io`; the Onstacks backend forwards internally. No core-service access or internal service key is needed. Keep the capability private. Browser callbacks and hash submission are not payout confirmation.

### Response and payment states

POST and GET return the same public envelope (example; placeholders are not credentials):

```json Public API response theme={"system"}
{
  "data": {
    "paymentRef": "12345678-1234-1234-1234-123456789abc",
    "merchantRef": "order-123",
    "environment": "sandbox",
    "status": "funding_submitted",
    "provider": "paycrest",
    "fundingHash": "0x...actual-broadcast-hash",
    "payoutStatus": "pending"
  },
  "error": null,
  "request_id": "support-request-id"
}
```

| `data.status` | Meaning / action |
| - | - |
| `pending` | Waiting for funding. Send only after verification and your own duplicate checks. |
| `funding_submitted` | Hash accepted; receipt/finality checks may still be pending. Poll; do not send again. |
| `payout_submitted` | Funding verified and payout tracking underway. Bank settlement is not yet confirmed. Poll. |
| `completed` | Onstacks verified funding and confirmed provider bank settlement. Stop funding and polling. |
| `needs_review` | Outcome is uncertain or needs intervention. Reconcile with Onstacks using the payment reference and saved hash. Do not resend or create a replacement to retry this payment. |

`data.payoutStatus` is the provider/local payment status and may be null. Use `data.status` for the overall delegated flow. HTTP 200 confirms the request was handled, not that funds reached the bank. An immediate POST can already return a later state if verification progressed.

Other lifecycle states (`creating`, `provider_requested`, `payout_claimed`) mean work is underway: wait, do not fund again. `retryable` permits resuming intent preparation through the widget; first reconcile any wallet broadcast in your own ledger. HTTP 409 means a conflicting funding hash: reconcile rather than replace it. On transient server errors, retry the same saved hash or poll status.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.