Metadata-Version: 2.5
Name: hashry-pay
Version: 0.1.0
Summary: Official Hashry Pay server SDK: create payments, read their status and verify webhooks.
Project-URL: Homepage, https://docs.hashrytech.com/pay
Project-URL: Documentation, https://docs.hashrytech.com/pay
Project-URL: Support, https://docs.hashrytech.com/pay#support
Author-email: Hashry Technologies <support@hashrytech.com>
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: caribbean,checkout,hashry,payments,webhooks
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx<1,>=0.27
Description-Content-Type: text/markdown

# hashry-pay

The official Hashry Pay server SDK for Python. Use it on your server to create
payments, read their status and verify webhooks.

Full docs: https://docs.hashrytech.com/pay

This is an early release (`0.0.1`). The API can change before `1.0`.

## Install

```sh
pip install hashry-pay
```

Python 3.11 or newer. The only dependency is `httpx`.

## Quick start

### 1. Create the client

Get the client ID and secret from your channel's API access page in the Hashry
admin. Keep them in environment variables on your server.

```python
import os
from hashry_pay import Hashry

hashry = Hashry(
    client_id=os.environ["HASHRY_CLIENT_ID"],
    client_secret=os.environ["HASHRY_CLIENT_SECRET"],
    environment="sandbox",  # "live" with a live key
)
```

A test key (`hsk_test_`) works only with `environment="sandbox"`, and a live key
(`hsk_live_`) only with `"live"`. The client refuses the wrong pair. Keys made
before the prefixes still work, with a one-time warning.

### 2. Create a payment on your server

```python
intent = hashry.payment_intents.create(
    amount="2500.00",  # a string or Decimal, never a float
    currency="JMD",
    description=f"Order #{order.number}",
    external_reference=str(order.id),
    payer={"email": order.email},  # an email or a phone is required
    return_url="https://shop.example.com/pay/done",
    cancel_url="https://shop.example.com/cart",
    presentation="embedded",
    embed_origin="https://shop.example.com",
    idempotency_key=f"order-{order.id}-pay-1",
)
```

You choose the `idempotency_key`, one per payment. The SDK never makes one up.
Sending the same key and details again is safe: you get the same payment back.

**A declined card does not end an embedded payment.** The SDK always asks for retry after a decline
(`retry_after_decline=True`) on an embedded create. It is not a parameter, so you cannot turn it off.
After a declined card the frame shows the card form again and the payment stays `redirect_issued`.
No `payment.failed` webhook is sent for that card. When the last card the frame allows (3) is
declined, the payment becomes `requires_payment_method`; then start a new attempt with
`create_attempt`. The docs have a complete route that starts, renews and retries a payment:
https://docs.hashrytech.com/pay/python. The SDK raises `HashryError` if the API answers an embedded create without
`retry_after_decline` on.

### 3. Hand four values to the browser

```python
return {
    "embed_url": intent.embed_url,
    "transaction_uid": intent.transaction_uid,
    "status_url": intent.status_url,
    "status_token": intent.status_token,
}
```

- `embed_url` and `transaction_uid` load the card form (with
  `@hashrytech/pay-react` or `@hashrytech/pay-svelte`). `embed_url` is a bearer
  link: don't log it or store it.
- `status_url` and `status_token` let the page show the status while the
  shopper waits (`pollStatus`). The token reads only this payment's status. It
  can't confirm a payment.

Send nothing else. The client secret never goes to the browser.

### 4. Confirm on your server

When the card form says it is done, or the shopper lands on your `return_url`,
ask Hashry. `refresh` checks with the gateway first.

```python
intent = hashry.payment_intents.refresh(intent_uid)
if intent.status == "succeeded":
    mark_order_paid(intent.external_reference, intent.uid)
```

### 5. Verify webhooks

```python
from django.db import transaction
from hashry_pay import WebhookError, webhooks


def hashry_webhook(request):  # Django shown; any framework works
    try:
        event = webhooks.verify(
            request.body,  # the raw bytes, not parsed JSON
            path=request.path,
            headers=request.headers,
            secret=os.environ["HASHRY_WEBHOOK_SECRET"],
        )
    except WebhookError:
        return HttpResponse(status=400)
    # Read from Hashry before opening your database transaction.
    intent = hashry.payment_intents.refresh(event.payment_intent_uid) if event.payment_intent_uid else None
    with transaction.atomic():
        if not seen_events.add_if_new(event.id):
            return HttpResponse(status=200)  # already handled
        if intent is not None:
            apply_payment_update(intent)  # your amount, currency, and order checks
    return HttpResponse(status=200)
```

`verify` checks the signature in constant time and refuses a timestamp more
than 5 minutes old or ahead. During a webhook secret rotation, pass
`secrets=[new_secret, old_secret]`. There is no way to read an event without
verifying it.

## Other resources

| Resource | Methods |
| --- | --- |
| `payment_intents` | `create`, `retrieve`, `list`, `refresh`, `cancel`, `create_attempt` |
| `payment_links` | `create`, `retrieve`, `list`, `cancel` |
| `payment_methods` | `list`, `delete`, `forget_customer` |
| `subscription_intents` | `create`, `retrieve`, `list` |
| `subscriptions` | `retrieve`, `list`, `charges`, `cancel`, `uncancel`, `update_card_session`, `retry_now` |
| `channel` | `self` |

`AsyncHashry` has the same methods. Use `await`, and `async with` or `aclose()`.

Results are typed objects. Their `raw` attribute holds the full response, so new
API fields are there before the SDK models them. Money values are strings with 2
decimal places.

## Errors and retries

- Every error is a `hashry_pay.HashryError`.
- API refusals raise `ApiError` with `code`, `status` and `detail`, or a
  subclass: `InvalidRequestError`, `AuthenticationError`,
  `PermissionDeniedError`, `NotFoundError`, `ConflictError`,
  `IdempotencyError`, `RateLimitError`, `ServerError`.
- No answer raises `ApiConnectionError` (or `ApiTimeoutError`).
- `error.outcome_unknown` is true when Hashry may have acted anyway. Replay with
  the same idempotency key, or read the payment. Never create a new one.

The SDK retries network errors, 5xx and 429 answers twice (`max_retries`), with
backoff. It retries only reads and calls with an idempotency key. Other 4xx
answers are never retried. The access token is cached and renewed before it
expires, and once more after a 401.

## Security rules

- Keep the client secret (`hsk_live_`, `hsk_test_`) and the webhook secret
  (`hwhsec_`) on your server. Use `@hashrytech/pay-build-guard` to fail a
  frontend build that bundles one.
- Give the browser only the intent `uid`, `embed_url`, `transaction_uid` and
  `status_token` (`hst_`).
- Treat the card form's "completed" message and webhooks as cues. Confirm with
  `refresh` or `retrieve` before you fulfil an order.
- Store each webhook event ID and apply each event once.
- The SDK masks secrets, the access token, `embed_url` and status tokens in
  `repr`, logs (logger `hashry_pay`) and errors.

## Supported versions

Python 3.11 or newer. CI tests 3.11, 3.12, 3.13 and 3.14.

## Support and security

Questions: **support@hashrytech.com**. For a security problem, email the same
address with "Security" in the subject.

## License

Apache License 2.0. See `LICENSE` and `NOTICE`.


Webhook deduplication uses the event ID inside the signed body. The verifier
rejects a different `X-Hashry-Event-Id` header. For older deliveries without
a signed event ID, it returns a stable hash of the signed body. Treat the
returned ID as opaque. Commit the ID and your payment changes in one database
transaction. A failed transaction must leave the event available for retry.
