Metadata-Version: 2.4
Name: livepasses
Version: 0.3.0
Summary: Official Python SDK for the Livepasses API - Digital wallet pass management
Author-email: Livepasses <sdk@livepasses.com>
License-Expression: MIT
Project-URL: Homepage, https://livepasses.com
Project-URL: Documentation, https://docs.livepasses.com
Project-URL: Repository, https://github.com/livepasses/livepasses-python
Project-URL: Issues, https://github.com/livepasses/livepasses-python/issues
Keywords: livepasses,wallet,apple-wallet,google-wallet,passes,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-httpx>=0.34.0; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Dynamic: license-file

# Livepasses Python SDK

Official Python SDK for the [Livepasses API](https://livepasses.com) — the leading API-first digital wallet pass platform.

Generate and manage Apple Wallet and Google Wallet passes for events, loyalty programs, coupons, and more.

## Installation

```bash
pip install livepasses
```

**Requirements**: Python 3.10+

## Quick Start

```python
from livepasses import Livepasses, GeneratePassesParams, PassRecipient, CustomerInfo, BusinessData

client = Livepasses("your-api-key")

result = client.passes.generate(GeneratePassesParams(
    template_id="tmpl-001",
    passes=[PassRecipient(
        customer=CustomerInfo(first_name="Alice", last_name="Smith", email="alice@example.com"),
        business_data=BusinessData(section_info="VIP", row_info="A", seat_number="1"),
    )],
))

for p in result.passes:
    print(f"Pass: {p.id} — Apple: {p.platforms.apple.add_to_wallet_url}")
```

## Authentication

Get your API key from the [Livepasses Dashboard](https://dashboard.livepasses.com).

```python
from livepasses import Livepasses

client = Livepasses("lp_live_your_api_key")
```

## Pass Generation

### Single Pass

```python
from livepasses import (
    GeneratePassesParams, PassRecipient, CustomerInfo, BusinessData, BusinessContext, EventContext
)

result = client.passes.generate(GeneratePassesParams(
    template_id="tmpl-001",
    business_context=BusinessContext(
        event=EventContext(event_name="Concert", event_date="2026-06-15T20:00:00Z"),
    ),
    passes=[PassRecipient(
        customer=CustomerInfo(first_name="John", last_name="Doe", email="john@example.com"),
        business_data=BusinessData(section_info="Floor", row_info="B", seat_number="15"),
    )],
))
```

### Batch Generation with Polling

For large batches, the API processes passes asynchronously. `generate_and_wait` handles polling automatically:

```python
from livepasses import GenerateAndWaitOptions

result = client.passes.generate_and_wait(
    GeneratePassesParams(
        template_id="tmpl-001",
        passes=[...],  # hundreds of recipients
    ),
    options=GenerateAndWaitOptions(
        poll_interval=2.0,
        max_attempts=150,
        on_progress=lambda status: print(f"Progress: {status.progress_percentage}%"),
    ),
)

print(f"Generated {len(result.passes)} passes")
```

### Batch Status (Manual Polling)

If you need more control over polling, use `generate` + `get_batch_status`:

```python
initial = client.passes.generate(GeneratePassesParams(
    template_id="tmpl-001",
    passes=[...],
))

if initial.batch_operation:
    import time
    batch_id = initial.batch_operation.batch_id
    status = client.passes.get_batch_status(batch_id)
    while not status.is_completed:
        time.sleep(2)
        status = client.passes.get_batch_status(batch_id)
        print(f"Progress: {status.progress_percentage}%")
    print(f"Completed: {status.statistics.successful} successful, {status.statistics.failed} failed")
```

## Pass Lifecycle

### Lookup

```python
from livepasses import LookupPassParams

pass_info = client.passes.lookup(LookupPassParams(pass_id="pass-001"))
print(f"Valid: {pass_info.is_valid}, Status: {pass_info.status}")
```

### Validate

```python
validation = client.passes.validate("pass-001")
print(f"Can redeem: {validation.can_be_redeemed}")
```

### Redeem

```python
from livepasses import RedeemPassParams, RedemptionLocation

# Generic redemption
result = client.passes.redeem("pass-001")

# Redemption with location; free-text notes go in metadata
result = client.passes.redeem("pass-001", RedeemPassParams(
    location=RedemptionLocation(name="Store #1", latitude=4.6097, longitude=-74.0817),
    metadata={"note": "Walk-in customer"},
))
```

### Check-in (Events)

Coordinates go inside `location`:

```python
from livepasses import CheckInParams, RedemptionLocation

result = client.passes.check_in("pass-001", CheckInParams(
    gate="Main Gate",
    location=RedemptionLocation(name="Main Gate", latitude=4.6097, longitude=-74.0817),
))
```

### Redeem Coupon

```python
from livepasses import RedeemCouponParams, RedemptionLocation

result = client.passes.redeem_coupon("pass-001", RedeemCouponParams(
    location=RedemptionLocation(name="Store #42"),
    transaction_amount=45000,
    transaction_currency="COP",
    metadata={"note": "Applied to order #12345"},
))
```

The API refuses any body field it does not declare with a `400`, so there is no `notes`
parameter: put free text in `metadata`.

### Update a Pass

Change fields on one pass and, optionally, show the holder a message. `updated_fields`
keys are the pass type's updatable field names as the API spells them (camelCase) and are
sent exactly as written. Send a non-empty `updated_fields`, a non-empty `message_body`, or both.

```python
from livepasses import UpdatePassParams

client.passes.update("pass-001", UpdatePassParams(
    updated_fields={"memberTier": "Platinum", "validUntil": "2026-12-31"},
    reason="Tier upgrade",
    message_body="Congratulations on reaching Platinum!",
))

# Silent change: no banner on the holder's phone
client.passes.update("pass-001", UpdatePassParams(
    updated_fields={"memberTier": "Platinum"},
    notify=False,
))
```

### Push a scoped update

Push field updates to all eligible passes of a template:

```python
from livepasses import PushTemplatePassesParams

client.passes.push_template("template-id", PushTemplatePassesParams(
    updated_fields={"gate": "Gate C"},
    reason="Event-wide gate change",
))
```

## Pass Types

### Event Tickets

```python
business_data = BusinessData(
    section_info="VIP", row_info="A", seat_number="1",
    ticket_type="VIP", price=150.00, currency="USD",
)
```

### Loyalty Cards

```python
from livepasses import LoyaltyTransactionParams

business_data = BusinessData(
    membership_number="MEM-12345",
    current_points=500,
    member_tier="Gold",
)

# Earn points
result = client.passes.loyalty_transact("pass-001", LoyaltyTransactionParams(
    transaction_type="earn", points=100, description="Purchase at Store #42",
))

# Spend points
result = client.passes.loyalty_transact("pass-001", LoyaltyTransactionParams(
    transaction_type="spend", points=50, description="Redeemed: Free coffee",
))
```

### Coupons

```python
business_data = BusinessData(
    promo_code="SUMMER2026",
    campaign_id="camp-001",
    max_usage_count=1,
)
```

## Templates

### List and get

```python
from livepasses import ListTemplatesParams

# List
templates = client.templates.list(ListTemplatesParams(status="Active"))

# Get details
detail = client.templates.get("tmpl-001")
```

### Create a template

```python
from livepasses import CreateTemplateParams

template = client.templates.create(CreateTemplateParams(
    name="VIP Event Pass",
    description="Premium event ticket template",
    # The block you send decides the template type: "event" makes an event ticket.
    business_features={
        "event": {
            "eventName": "Aurora Music Fest",
            "eventDate": "2030-06-15T20:00:00Z",
            "venueName": "Aurora Arena",
            "showSeatNumbers": True,
        },
    },
))
print(f"Created: {template.id} — {template.name}")
```

### Update a template

```python
from livepasses import UpdateTemplateParams

updated = client.templates.update("tmpl-001", UpdateTemplateParams(
    name="VIP Event Pass v2",
    description="Updated premium event ticket template",
))
```

### Activate / Deactivate

```python
client.templates.activate("tmpl-001")
client.templates.deactivate("tmpl-001")
```

## Webhooks

```python
from livepasses import CreateWebhookParams

webhook = client.webhooks.create(CreateWebhookParams(
    url="https://your-app.com/webhooks/livepasses",
    events=["pass.generated", "pass.redeemed", "pass.sharing_suspected"],
))
print(f"Secret: {webhook.secret}")  # use this to verify webhook signatures

# List all
webhooks = client.webhooks.list()

# Delete
client.webhooks.delete(webhook.id)
```

## Error Handling

Every refusal the API can make answers with a real HTTP status (`400`/`403`/`404`/`409`/`422`/`429`/`500`/`502`/`503`) and the envelope `{success:false,data:null,error:{code,message,details,timestamp,traceId,fields?}}`. The SDK raises a typed error from **any** status or body — including a status-only response it can't parse as JSON, such as a challenge `401` or a proxy error. All errors are typed for precise `except` handling:

```python
from livepasses import (
    LivepassesError,
    AuthenticationError,
    ValidationError,
    ForbiddenError,
    NotFoundError,
    RateLimitError,
    QuotaExceededError,
    BusinessRuleError,
)

try:
    result = client.passes.generate(params)
except AuthenticationError:
    print("Invalid API key")
except ValidationError as e:
    print(f"Validation failed: {e.details}")
except ForbiddenError:
    print("Insufficient permissions for this operation")
except NotFoundError:
    print("Template or pass not found")
except RateLimitError as e:
    print(f"Rate limited — retry after {e.retry_after}s")
except QuotaExceededError:
    print("API quota exceeded — upgrade your plan")
except BusinessRuleError as e:
    print(f"Business rule violation: {e}")
except LivepassesError as e:
    # Catch-all for any other API error
    print(f"API error [{e.code}]: {e} (HTTP {e.status})")
```

### Error codes

Use `ApiErrorCodes` for programmatic error code comparisons:

```python
from livepasses import LivepassesError, ApiErrorCodes

try:
    client.passes.redeem("pass-001")
except LivepassesError as e:
    if e.code == ApiErrorCodes.PASS_ALREADY_USED:
        print("This pass has already been redeemed")
    elif e.code == ApiErrorCodes.PASS_EXPIRED:
        print("This pass has expired")
    elif e.code == ApiErrorCodes.TEMPLATE_INACTIVE:
        print("The template for this pass is inactive")
    else:
        print(f"Unhandled error: {e.code}")
```

### Exception hierarchy

| Exception | Typical status | When |
|-----------|------------|------|
| `AuthenticationError` | 401 | Invalid, expired, or revoked API key |
| `ValidationError` | 400 | Request validation failed — carries `fields: dict[str, list[str]] \| None`, the field path -> validation messages map; keys are the API's camelCase field paths (e.g. `operations[0].path`), not converted to snake_case |
| `ForbiddenError` | 403 | Insufficient permissions |
| `NotFoundError` | 404 | Resource not found |
| `RateLimitError` | 429 | Rate limit exceeded |
| `QuotaExceededError` | 422 | API quota or subscription limit exceeded |

The status column is the one each class usually carries; the error's `.status` is always the response's real HTTP status. A `401` is always the authentication error and a `403` always the forbidden error, whatever `error.code` says. A `409` without a mapped code, and every `5xx`, raise the base `LivepassesError`.
| `BusinessRuleError` | 422 | Business rule violation (pass expired, already used, etc.) |

## Pagination

### Manual pagination

```python
from livepasses import ListPassesParams

page1 = client.passes.list(ListPassesParams(page=1, page_size=50))
print(f"Page 1 of {page1.pagination.total_pages} ({page1.pagination.total_items} total)")

# Get next page
if page1.pagination.current_page < page1.pagination.total_pages:
    page2 = client.passes.list(ListPassesParams(page=2, page_size=50))
```

### Auto-pagination

Iterate over all passes without manual page management:

```python
for p in client.passes.list_auto_paginate():
    print(f"{p.id}: {p.status}")
```

## Configuration

```python
client = Livepasses(
    "your-api-key",
    base_url="https://api.livepasses.com",  # default
    timeout=30.0,                            # seconds, default
    max_retries=3,                           # default
)
```

### Automatic Retries

The SDK automatically retries:
- **429 Too Many Requests** — honors `Retry-After` header
- **5xx Server Errors** — exponential backoff with jitter, **only for idempotent methods** (`GET`, `HEAD`, `PUT`, `DELETE`). A `POST` that hits a `5xx` is not retried — no SDK sends an `Idempotency-Key`, so a retry could re-run a non-idempotent operation.

## Type Checking

This package ships with a `py.typed` marker (PEP 561) and is fully compatible with `mypy` and `pyright`.

```bash
mypy your_app.py   # full type safety
```

## Examples

See the [`examples/`](./examples/) directory for runnable scripts:

- **[generate_passes.py](./examples/generate_passes.py)** — End-to-end pass generation, lookup, validation, and check-in
- **[loyalty_workflow.py](./examples/loyalty_workflow.py)** — Loyalty card lifecycle: generate, earn points, spend points, update tier
- **[coupon_workflow.py](./examples/coupon_workflow.py)** — Coupon pass generation and redemption
- **[template_management.py](./examples/template_management.py)** — CRUD operations on pass templates
- **[webhook_setup.py](./examples/webhook_setup.py)** — Register, list, and manage webhooks

Run any example with:
```bash
export LIVEPASSES_API_KEY="your-api-key"
python examples/generate_passes.py
```

## License

MIT
