Metadata-Version: 2.4
Name: turtleqr
Version: 0.1.0
Summary: Official TurtleQR API client. Dynamic QR codes that keep working even after you stop paying.
License: UNLICENSED
Project-URL: Homepage, https://turtleqr.com
Project-URL: Documentation, https://api.turtleqr.com/docs
Keywords: qr,qr-code,turtleqr,dynamic-qr,api,sdk
Requires-Python: >=3.8
Description-Content-Type: text/markdown

# turtleqr (Python)

Official Python client for the [TurtleQR](https://turtleqr.com) API.

TurtleQR makes dynamic QR codes that **keep working even after you stop paying**.
This client is a thin, **zero-dependency** wrapper over the REST API (standard
library only, Python 3.8+).

## Install

```bash
pip install turtleqr
```

## Quickstart

```python
from turtleqr import TurtleQR

tq = TurtleQR(api_key="qr_live_...")

# Create a dynamic URL code (fields are the API's camelCase names)
code = tq.codes.create(type="url", targetUrl="https://example.com/launch", tags=["spring"])
print(code["shortUrl"])  # https://qr.turtleqr.com/AbC123xy

# Re-point it later without reprinting the QR
tq.codes.update(code["id"], targetUrl="https://example.com/sale")

# Analytics + raw image bytes
stats = tq.codes.analytics(code["id"], range="30d")
png = tq.codes.image(code["id"], format="png", size=1024)
```

## Methods

| Method | Endpoint |
| --- | --- |
| `codes.create(**fields)` | `POST /v1/codes` |
| `codes.list(...)` | `GET /v1/codes` |
| `codes.get(code_id)` | `GET /v1/codes/:id` |
| `codes.update(code_id, **fields)` | `PATCH /v1/codes/:id` |
| `codes.delete(code_id)` | `DELETE /v1/codes/:id` |
| `codes.bulk_create(codes)` | `POST /v1/codes/bulk` (up to 50) |
| `codes.scans(code_id, ...)` | `GET /v1/codes/:id/scans` |
| `codes.analytics(code_id, ...)` | `GET /v1/codes/:id/analytics` |
| `codes.image(code_id, ...)` | `GET /v1/codes/:id/image` |
| `codes.conversions(code_id)` | `GET /v1/codes/:id/conversions` |
| `conversions.create(**fields)` | `POST /v1/conversions` |
| `webhooks.create / list / get / update / delete` | `…/v1/webhooks` |
| `webhooks.test(id)` / `webhooks.deliveries(id)` | `…/v1/webhooks/:id/test` · `/deliveries` |

## Conversions, retargeting & webhooks

```python
# Record a conversion (from your server, after a sale/signup), then read the rollup
tq.conversions.create(codeId="code_1", eventName="purchase", valueCents=2599, currency="USD")
summary = tq.codes.conversions("code_1")
print(summary["totals"]["conversions"])

# Hosted-page codes can fire the page owner's pixels via `tracking` at create time
tq.codes.create(
    type="business-page",
    businessPage={"name": "Ada Lovelace", "links": []},
    tracking={"metaPixelId": "123456789012", "googleTagId": "G-ABC1234"},
)

# Webhooks
hook = tq.webhooks.create(url="https://example.com/turtleqr", event_filter=["code.created"])
print(hook["signingSecret"])  # shown once — store it now
tq.webhooks.test(hook["id"])
```

## Errors

Any non-2xx response raises `TurtleQRError` with `status`, `code`
(e.g. `slug_conflict`), and `detail`:

```python
from turtleqr import TurtleQRError

try:
    tq.codes.create(type="url", targetUrl="https://example.com", slug="taken")
except TurtleQRError as err:
    if err.code == "slug_conflict":
        ...  # pick another slug
    else:
        raise
```

## Verifying incoming webhooks

```python
from turtleqr import verify_webhook_signature

# Flask:
@app.route("/hook", methods=["POST"])
def webhook():
    ok = verify_webhook_signature(
        header=request.headers.get("Turtleqr-Signature", ""),
        body=request.get_data(as_text=True),   # read before json parsing
        secret=os.environ["TURTLEQR_WEBHOOK_SECRET"],
    )
    if not ok:
        abort(401)
    event = request.get_json()
    ...
```

## Tests

Pure standard library, no test deps:

```bash
cd clients/python && python3 -m unittest discover -s tests -t .
```
