Metadata-Version: 2.5
Name: vonpay-checkout
Version: 4.6.0
Summary: Von Payments Checkout SDK for Python
Project-URL: Homepage, https://github.com/Von-Payments/vonpay
Project-URL: Documentation, https://docs.vonpay.com
License-Expression: MIT
Keywords: checkout,payments,sdk,vonpay
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Description-Content-Type: text/markdown

# vonpay-checkout

Python SDK for the [Von Payments Checkout API](https://docs.vonpay.com). Create hosted checkout sessions, verify webhook signatures, and validate signed return redirects.

## Install

```bash
pip install vonpay-checkout
```

**Requires:** Python 3.9+, httpx 0.27+

## Quick start

```python
from vonpay.checkout import VonPayCheckout, VonPayError

client = VonPayCheckout("vp_sk_test_...")

session = client.sessions.create(
    amount=1499,
    currency="USD",
    country="US",
    success_url="https://example.com/order/123/confirm",
)

print(session.checkout_url)
# https://checkout.vonpay.com/checkout?session=vp_cs_test_...
```

### Discrete-lifecycle API

The `payment_intents`, `refunds`, `tokens`, and `capabilities` namespaces give server-side control over payment intents, alongside the existing hosted-checkout `sessions` flow.

```python
# Create a payment intent (auth + capture)
intent = client.payment_intents.create(
    amount=1499,
    currency="usd",
    capture_method="automatic",
    metadata={"order_id": "ord_42"},
    idempotency_key="ord_42-charge-1",
)
print(intent.id, intent.status)  # "vpi_live_…", "succeeded"

# Auth-only + later capture (fulfillment-on-ship)
auth = client.payment_intents.create(
    amount=1499, currency="usd", capture_method="manual", idempotency_key="ord_43-hold-1"
)
client.payment_intents.capture(auth.id, amount_to_capture=1499)

# Refund a captured intent (partial or full)
refund = client.refunds.create(payment_intent=intent.id, amount=500)

# Create a reusable token for save-card / MIT flows
token = client.tokens.create(buyer_id="buyer_42", setup_for_future_use="off_session")

# List a buyer's saved, active cards (newest first; page with next_cursor / has_more)
cards = client.payment_methods.list(buyer_id="buyer_42")

# Your payments, newest first, with filters (one page; at most a 31-day window)
declines = client.payment_intents.list(outcome=["bank_declined"], store="shop.example.com")
# Every page, one request at a time (stops on has_more False, or after max_items).
# To resume later, use list_pages(): it hands you each page's next_cursor (valid 24 h with the same filters).
for pi in client.payment_intents.list_auto_paging(created_gte="2026-10-01T00:00:00Z", max_items=500):
    print(pi.id, pi.outcome)
# Approval and decline rates, computed as the dashboard counts payment attempts.
# A rate is None -- not 0 -- when there was no traffic.
summary = client.payment_intents.summary(group_by=["day"], time_zone="America/New_York")
# For an agent, use a restricted key holding only payment_intents.read.

# A signed-in shopper removes their own card — PERMANENT. A repeat returns already_revoked=True.
client.tokens.revoke(cards.data[0].id, buyer_id="buyer_42", initiated_by="buyer")

# Apple Pay on your own checkout domain: register, host the file, verify
domain = client.wallet_domains.create(domain="shop.example.com", wallet_type="apple_pay")
file = client.wallet_domains.retrieve_file(domain.id)  # exact bytes, not JSON
# serve file.content at https://shop.example.com/.well-known/apple-developer-merchantid-domain-association
checked = client.wallet_domains.verify(domain.id)  # read checked.verification_status

# Read the merchant's processor capability matrix before invoking optional ops
caps = client.capabilities.get()
if caps.supported_operations.partial_refund:
    # partial refunds supported — safe to offer partial-refund UI
    ...
```

**Send `idempotency_key` on every `payment_intents.create`.** Use one value per charge and send the same value on every retry of that charge. Some payment connections already refuse a charge without one, and the announced **28 October 2026** cut-off has passed: the API may refuse it with `400 idempotency_key_required` (nothing is charged). This SDK emits a one-time `FutureWarning`. 4.0.0 still accepts a charge without one; the key becomes required by this SDK in a later major version.

### Subscription page link ("Manage subscription")

For a customer you have already signed in, create a one-time link to their subscription page (view, pause, resume, cancel, update card) when they press the button, and send them straight to it. The link opens once, within five minutes.

```python
# On the button press. A fresh idempotency key is generated per call unless you
# pass one; never reuse one key across presses.
link = client.subscriptions.create_portal_link(
    "vsub_live_k7x9m2n4p3q5r6s8",
    return_url="https://shop.example.com/account",
)
# Return link.url to the page with "Cache-Control: no-store" and navigate from
# the browser (keep the # part -- the code is in it).
```

Never email, log or store the link, never put it in a query string, and never send it through anything that previews links. `404 subscription_not_found` means there is nothing to manage (hide the button); `503 not_switched_on` means links are not switched on for your account.

### Subscription sign-up: show the terms, record the agreement

Read the product's terms before the order button, show them with your business name and how to cancel, then send what you showed on the sign-up. A missing or unusual record never refuses the charge: the response's `warnings` say what to fix.

```python
from vonpay.checkout import PaymentMethodRef, RecurringBlock, RecurringConsent

# Before the page renders: the terms for the membership product.
terms = client.subscription_products.terms("633")
shown = terms.disclosure + " Cancel anytime from your account. Billed by EXAMPLE CO."

# On the order button: the card the shopper saved on this page, plus what they agreed to.
pi = client.payment_intents.create(
    payment_method=PaymentMethodRef(id=saved_card_id),
    buyer_email="shopper@example.com",
    buyer_ip_address=shopper_ip,
    recurring=RecurringBlock(
        product="633",
        consent=RecurringConsent(
            method="checkbox", accepted_at=clicked_at, disclosure_shown=shown, user_agent=user_agent
        ),
    ),
    idempotency_key=order_id,
)
for w in pi.warnings or []:
    logger.warning("[subscription] %s: %s", w.code, w.message)
```

`shown` must contain `terms.price_phrase` (case and spacing are ignored), or the response warns `recurring_disclosure_differs`. An unknown product is `404 subscription_product_not_found`.

## Migrating from 3.x

4.0.0 changes two behaviours. Nothing in the 3.x API was removed or renamed.

1. **A live secret key is sent only to Vonpay's API.** With a `vp_sk_live_*` key, `VonPayCheckout(...)` raises `ValueError` unless `base_url` is unset or https on `checkout.vonpay.com` / `checkout-staging.vonpay.com`. Remove `base_url`, or, for a proxy you run on purpose, pass `allow_custom_base_url=True` (the environment variable `VONPAY_ALLOW_CUSTOM_BASE_URL=1` on its own is deprecated: in 4.x it still opts a live key in when you omit the argument, with a one-time `FutureWarning`, and from 5.0.0 it is refused). Test and publishable keys are never refused; with an address that is not a Vonpay host or this machine they emit a one-time `CustomBaseUrlWarning`, which raises under `-W error`.
2. **An unknown money outcome says "check first".** When a charge, capture, void or refund fails ambiguously and the SDK does not repeat it (`err.retry_withheld is True`), the error now has `retryable=False` and `next_action="reconcile"`. Read the payment back (`payment_intents.retrieve`, or `refunds.list` for a refund) and send it again only if it did not happen, with the same `idempotency_key`.
3. **Webhook `card` can be absent** from `data` on the charge, refund and saved-card events: read it with `event.data.get("card")`.

## Migrating from 2.x

3.0.0 changes two behaviours. Nothing else in the 2.x API was removed or renamed.

1. **`payment_intents.void()` is retried automatically only when you pass an idempotency key.**
   After a timeout or a `500`/`502`/`503`/`504`, 2.x retried a void on its own. 3.x does that only
   when the call carries a key, exactly as it already did for `capture()` and `refunds.create()`.
   Without one, the first error is raised with `retry_withheld=True`. To keep automatic retries:

   ```python
   client.payment_intents.void(payment_intent_id, idempotency_key=f"{order_id}-void")
   ```

2. **A redirect from the API is now its own error, `unexpected_redirect`.** The API never
   redirects, and this SDK has never followed one. 3.x raises a `VonPayError` with
   `code == "unexpected_redirect"`, `retryable is False` and `next_action == "check_configuration"`
   (2.x raised a generic error). If you see it, check your `base_url` (the API origin itself,
   `https://checkout.vonpay.com`: https, no path), any proxy, and your network. Do not retry it.
   If you branch exhaustively on `err.next_action`, add the new `"check_configuration"` value.

## Features

- **Typed session / webhook / error objects** — full `CheckoutSession`, `SessionStatus`, `WebhookEvent`, `VonPayError` types.
- **Webhook verification** — `webhooks.construct_event(raw_body, signature_header, signing_secret)` parses the `x-vonpay-signature: t=<unix>,v1=<hex>` header, verifies HMAC-SHA256 over `f"{t}.{raw_body}"` keyed by your per-endpoint signing secret (`whsec_…`), and enforces the freshness window (≤5 min old / ≤30 sec future). Accepts multiple `v1=` entries for zero-downtime secret rotation.
- **Test events — check `event.test_event` FIRST** (2.7.0+). It is `True` only on an event synthesized by "Send test event"; `livemode` does not tell you this. Return 2xx and do nothing else — a test event can carry a real session's ids.
- **Re-read a payment** — `payment_intents.retrieve(id)` (2.7.0+) returns its stored state. On a 3-D Secure return page use it, with the id you stored at create time, instead of the URL's query params.
- **Return confirmation** — `client.sessions.confirm_return(params, session_secret)` verifies the signature **and** confirms server-side that the payment succeeded, returning `ReturnOutcome(paid, signature_valid, status, reason)`. **Use this, and branch on `paid`.** ⛔ **`verify_return_signature()` is DEPRECATED and STOPS WORKING on or after 2026-10-13** — the redirect stops carrying `sig` / `status` / `amount` / `currency` / `transaction_id` at all, so there will be nothing to verify and it will refuse every return. Migrate before that date. (It was also never usable as merchant-side proof: the signature uses a platform-wide secret no merchant was ever issued.) It proves a message is AUTHENTIC, and a **declined payment is signed just as validly**, so a `true` from it was never proof of payment either. ⚠️ `reason == "still_pending"` means the charge is in flight (the ordinary 3-D Secure case) — show a neutral "confirming your payment", never a failure. ⚠️ `paid: true` still does not mean safe to fulfil: record which session IDs you have already fulfilled, and prefer fulfilling from the `charge.succeeded` webhook. ⚠️ **Not `session.succeeded`** — that event is emitted internally but is **not subscribable**: it is absent from the merchant subscription catalog, which accepts an unknown event key, stores nothing and returns success. An endpoint subscribed to it receives nothing, forever, with no error raised at any layer. `charge.succeeded` is the fulfilment event.
- **Tie the return to YOUR order** — any of your paid sessions answers `paid=True`, so a buyer could paste the return link of a different, cheaper order they paid for. Pass what your server recorded when it created the session: `confirm_return(params, expected_session_id=..., expected_amount=..., expected_currency=...)` (the session id, or the amount + currency pair, or both). A mismatch is `paid=False` with `reason="session_mismatch"` or `"amount_mismatch"`, never paid; show a neutral "we couldn't match this payment to your order", not "payment failed". Optional; without it nothing changes.
- **Where your secret key may go** — every request carries your secret key, so `base_url` is checked when the client is created. Leave it unset (`https://checkout.vonpay.com` serves live and test keys). ⚠️ A **live** secret key (`vp_sk_live_*`) is sent only to https on `checkout.vonpay.com` or `checkout-staging.vonpay.com`; any other address raises `ValueError` at construction, unless you pass `allow_custom_base_url=True` in your code (only for a proxy you run on purpose). The environment variable `VONPAY_ALLOW_CUSTOM_BASE_URL=1` on its own is deprecated, because an environment can be set by something other than your code: in 4.x it still works when the argument is omitted, with a one-time `FutureWarning`; from 5.0.0 it is refused. An explicit `allow_custom_base_url=False` always wins over it. A test key may also use this machine (`localhost`, `127.0.0.1`, `[::1]`); any other address is used with a one-time `CustomBaseUrlWarning` naming it (filter it under `-W error` with `ignore::vonpay.checkout.CustomBaseUrlWarning`). `key_destination_problem(api_key, base_url, allow_custom=False)` gives the same answer without creating a client (`None`, or the reason).
- **Auto-retry** — exponential backoff on 429 / 5xx with `Retry-After` header support.
- **When a payment's outcome is unknown** — if a charge, capture, void or refund gets a `500`/`502`/`503`/`504` or a reply that cannot be read, and the SDK does not retry it, the error has `retry_withheld=True`, `retryable=False` and `next_action="reconcile"`, whatever its `code`. Money may have moved: read the payment back (`payment_intents.retrieve`, or `refunds.list` for a refund) before anything else. If it did not happen and you send it again, reuse the **same** `idempotency_key`, never a new one. (`RETRY_WITHHELD_HELP` holds this advice.)
- **Plugin identification** — building a plugin other merchants install? Pass `app_info={"name": ..., "version": ..., "url": ...}` and it is appended to the `User-Agent` of every request.
- **Request ID tracing** — every response includes `X-Request-Id` for support tickets.

## Documentation

- [Python SDK reference](https://docs.vonpay.com/sdks/python-sdk)
- [API reference](https://docs.vonpay.com/reference/api)
- [Quickstart](https://docs.vonpay.com/quickstart)

## License

MIT
