> ## Documentation Index
> Fetch the complete documentation index at: https://devdocs.paywithatoa.co.uk/llms.txt
> Use this file to discover all available pages before exploring further.

# Collect — money in

> Collect money with Agent Pay: take a customer-present pay-link, or charge off-session under a COLLECT contract the customer approved once.

Two shapes: the customer is present and pays a link, or they're not and you charge under a **COLLECT contract**
they approved once. No card or method parameters in either — Atoa settles with the method on file.

## Pay-link — customer present

<CodeGroup>
  ```python Python theme={null}
  req = atoa.payment.collect(
      amount={"amount": 45.00},    # decimal major units; GBP by default
      order_id="order-1099",       # your own reference
  )

  print(req.payment_url)           # hand over the link
  print(req.qr_code_url)           # or the QR

  settled = atoa.payment.await_settled(req.payment_request_id)
  print(settled.status)            # COMPLETED
  ```

  ```typescript TypeScript theme={null}
  const req = await atoa.payment.collect({
    amount: { amount: 45.00 },     // decimal major units; GBP by default
    orderId: "order-1099",         // your own reference
  });

  console.log(req.paymentUrl);     // hand over the link
  console.log(req.qrCodeUrl);      // or the QR

  const settled = await atoa.payment.awaitSettled(req.paymentRequestId);
  console.log(settled.status);     // COMPLETED
  ```
</CodeGroup>

* `expiresIn` (milliseconds) shortens the link's window; an unpaid link reads back `EXPIRED`.
* Cancel an unpaid link with `payment.cancel(paymentRequestId)` → `CANCELLED`, reason `MERCHANT_CANCELLED`.

**Sandbox:** open the link, choose the **Atoa Test Bank**, and pick the outcome — `COMPLETED`, `FAILED`, or
`PENDING`. Cancelling on the bank page gives `CUSTOMER_CANCELLED`.

## Off-session — under a COLLECT contract \[#off-session]

The customer approves the contract (and links a payment method) once at its `authorizationUrl`. After that you
charge under the contract while they're away — pass the `contractId`, never a payment method.

<CodeGroup>
  ```python Python theme={null}
  customer = atoa.customer.create(
      full_name="Ada Lovelace",
      email="ada@example.com",      # email or phone required
  )

  contract = atoa.contract.create(
      type="COLLECT",
      name="Monthly membership",
      atoa_customer_id=customer.id,
      limits={
          "max_per_payment": 50.00,
          "period_limits": [{"amount": 100.00, "period": "MONTH"}],
          "valid_to": "2026-12-31T23:59:59Z",
      },
  )

  print(contract.authorization_url)                # customer links a method + approves the caps here
  atoa.contract.await_active(contract.contract_id)          # → ACTIVE

  charge = atoa.payment.collect(
      contract_id=contract.contract_id,
      atoa_customer_id=customer.id,
      amount={"amount": 20.00},
      order_id="membership-jan",
  )
  # charge.next_action — the customer approves before it settles (next section)
  ```

  ```typescript TypeScript theme={null}
  const customer = await atoa.customer.create({
    fullName: "Ada Lovelace",
    email: "ada@example.com",       // email or phone required
  });

  const contract = await atoa.contract.create({
    type: "COLLECT",
    name: "Monthly membership",
    atoaCustomerId: customer.id,
    limits: {
      maxPerPayment: 50.00,
      periodLimits: [{ amount: 100.00, period: "MONTH" }],
      validTo: "2026-12-31T23:59:59Z",
    },
  });

  console.log(contract.authorizationUrl);          // customer links a method + approves the caps here
  await atoa.contract.awaitActive(contract.contractId);    // → ACTIVE

  const charge = await atoa.payment.collect({
    contractId: contract.contractId,
    atoaCustomerId: customer.id,
    amount: { amount: 20.00 },
    orderId: "membership-jan",
  });
  // charge.nextAction — the customer approves before it settles (next section)
  ```
</CodeGroup>

Atoa enforces the `limits` on every charge. Check headroom before charging instead of probing with a failure:

<CodeGroup>
  ```python Python theme={null}
  usage = atoa.contract.get(contract.contract_id).usage or []
  month = next((u for u in usage if u.period == "MONTH"), None)
  print(month.remaining)           # headroom this window; over-cap charges fail LIMIT_EXCEEDED
  ```

  ```typescript TypeScript theme={null}
  const { usage } = await atoa.contract.get(contract.contractId);
  const month = usage?.find((u) => u.period === "MONTH");
  console.log(month?.remaining);   // headroom this window; over-cap charges fail LIMIT_EXCEEDED
  ```
</CodeGroup>

## SCA on a charge

An off-session charge doesn't settle on its own. It returns an approval action — **`nextAction`** (TS) /
**`next_action`** (Python) — and the **customer** approves on Atoa's page with a one-time code or a passkey. Your
app never sees the credential.

<CodeGroup>
  ```python Python theme={null}
  share_with_customer(charge.next_action.approval_url)   # your own delivery — email, push, …
  decision = atoa.payment.await_decision(charge.next_action.approval_id)
  print(decision.status)                                  # APPROVED / DECLINED / EXPIRED

  settled = atoa.payment.await_settled(charge.payment_request_id)
  print(settled.status)                                   # COMPLETED
  ```

  ```typescript TypeScript theme={null}
  shareWithCustomer(charge.nextAction.approvalUrl);    // your own delivery — email, push, …
  const decision = await atoa.payment.awaitDecision(charge.nextAction.approvalId);
  console.log(decision.status);                        // APPROVED / DECLINED / EXPIRED

  const settled = await atoa.payment.awaitSettled(charge.paymentRequestId);
  console.log(settled.status);                         // COMPLETED
  ```
</CodeGroup>

To keep the approver in your own web UI, embed the approval with the
[**Approvals SDK**](/agent-pay/approvals) instead of sharing the URL — pass `nextAction.clientSecret` to
`confirmApproval`. Declined reads back `FAILED` with `failureReason: APPROVAL_DECLINED`; lapsed, `APPROVAL_EXPIRED` —
returned on the `Payment`, not thrown. In sandbox, open the `approvalUrl` and force whichever decision you want to test.

## Refunds

Refund a `COMPLETED` collect, in full or in part. A refund moves `INITIATED → COMPLETED` (or `FAILED`).

<CodeGroup>
  ```python Python theme={null}
  refund = atoa.payment.refund(
      req.payment_request_id,
      amount={"amount": 10.00},       # omit for a full refund
      reason="Damaged item",
  )
  print(refund.status, refund.refund_amount)

  atoa.payment.list_refunds(req.payment_request_id)       # all refunds on this payment
  ```

  ```typescript TypeScript theme={null}
  const refund = await atoa.payment.refund(req.paymentRequestId, {
    amount: { amount: 10.00 },      // omit for a full refund
    reason: "Damaged item",
  });
  console.log(refund.status, refund.refundAmount.amount);

  await atoa.payment.listRefunds(req.paymentRequestId);   // all refunds on this payment
  ```
</CodeGroup>

A pending refund can be cancelled with `cancelRefund(refundId)`. **Sandbox:** set `reason` to `"FAILURE TEST"` to
force a `FAILED` refund and exercise that branch.
