Reeds Python library
The official Python library for the Reeds APIs: one client for every Reeds service.
Developer docs · API reference · API changelog · Library changelog
from reeds import Reeds
from reeds.shipping import Quote
reeds = Reeds() # reads REEDS_CLIENT_ID and REEDS_CLIENT_SECRET
quote = reeds.shipping.quotes.create(pickup=pickup, dropoff=dropoff, package={"description": "Sneakers", "quantity": 1, "weight_kg": 1.5})
if isinstance(quote, Quote):
delivery = reeds.shipping.deliveries.create(quote_id=quote.id, reference="ORDER-10045")
print(delivery.tracking_code) # HQ3W5F2
Contents
- Services
- Why use the library
- Install
- Credentials and modes
- Shipping: quickstart, coverage and quotes, address search, deliveries, statuses, labels, wallet and invoices, sandbox
- Async
- Webhooks: FastAPI, Flask, Django
- Lists and pagination
- Errors
- Retries, idempotency and timeouts
- Types
- Configuration
- Versioning · Support · License
Services
Reeds offers its services as separate APIs. The library gives each one a property on the same client, so one install and one set of credentials cover them all:
| Service | On the client | Status |
|---|---|---|
| Shipping: address search, quotes, deliveries, labels, the wallet, invoices, the sandbox | reeds.shipping |
Available |
Webhooks are shared: one endpoint receives every service's events, all verified by reeds.webhooks. A new service arrives in a minor version of this library, so upgrading never breaks the services you already use.
Why use the library
| Authentication handled | It exchanges your client id and secret for an access token, caches it, and refreshes it before it expires. |
| Safe retries | Every POST carries an Idempotency-Key, and a retry reuses it, so a retry never books or charges twice. |
| Resilient | It retries rate limits, overload and dropped connections, honouring Retry-After. |
| Typed | Responses are Pydantic models and parameters are TypedDicts, generated from the API's OpenAPI spec. Your editor completes them; mypy and pyright check them. |
| Sync and async | Reeds for regular code, AsyncReeds for asyncio (FastAPI, aiohttp, Django async views). Same methods. |
| Pagination | A for loop (or async for) walks every page of a list. |
| Clear errors | One exception class per kind of failure, with the API's code, the invalid fields, a doc_url and the request_id. |
| Webhooks verified | construct_event checks the signature and returns a typed event. |
Install
pip install reeds-sdk
Or uv add reeds-sdk, or poetry add reeds-sdk. Import it as reeds. It needs Python 3.11 or later, and installs HTTPX2 and Pydantic 2.
Credentials and modes
Get your client id and secret from Dashboard → Developers. Each mode has its own pair:
| Mode | Client id | Client secret | What happens |
|---|---|---|---|
| Test | rci_test_... |
rcs_test_... |
Nothing real moves and no real money is spent. Use it to build and test. |
| Live | rci_live_... |
rcs_live_... |
Real pickups, real charges. Available once Reeds approves your business. |
import os
from reeds import Reeds
reeds = Reeds(client_id=os.environ["REEDS_CLIENT_ID"], client_secret=os.environ["REEDS_CLIENT_SECRET"])
reeds.mode # "test" or "live", from the client id
reeds.scopes() # the services your credentials can use, for example ["shipping"]
client_id and client_secret default to the REEDS_CLIENT_ID and REEDS_CLIENT_SECRET environment variables. Keep the secret on your server: never put it in a browser or a mobile app.
Shipping
The Shipping API books pickups and deliveries: within a state, and between states along the routes Reeds runs.
Quickstart
from reeds import Reeds
from reeds.shipping import QuoteUnavailable
reeds = Reeds()
# 1. Price it.
quote = reeds.shipping.quotes.create(
pickup={"name": "Tola Fabrics", "phone": "08051234567", "address": "23 Allen Avenue", "area": "Ikeja", "lga": "Ikeja", "state": "Lagos"},
dropoff={"name": "Chinedu Eze", "phone": "08098765432", "address": "14 Admiralty Way", "area": "Lekki Phase 1", "lga": "Eti-Osa", "state": "Lagos"},
package={"description": "2 pairs of sneakers", "quantity": 1, "weight_kg": 1.5},
)
if isinstance(quote, QuoteUnavailable):
# Not an error: Reeds doesn't serve this route. Tell your customer why.
raise SystemExit(quote.reason.message)
print(quote.fee.total, quote.schedule) # kobo, and the pickup and delivery promise
# 2. Book it. The wallet is charged and the delivery gets its tracking code.
delivery = reeds.shipping.deliveries.create(quote_id=quote.id, reference="ORDER-10045")
# 3. Follow it with webhooks (see below), or read it.
latest = reeds.shipping.deliveries.get(delivery.id)
print(latest.status, latest.tracking_url)
Parameters are keyword arguments, and field names are the API's own (quote_id, tracking_code), so your code reads like the API reference. Amounts are integers in kobo: 280000 is ₦2,800. Times are ISO 8601 strings in Lagos time (datetime.fromisoformat reads them).
Coverage and quotes
coverage = reeds.shipping.coverage.get() # the states, zones and inter-state routes Reeds serves
for state in coverage.states or []:
print(state.state, state.zones)
A quote holds its price for 24 hours. A route Reeds doesn't serve comes back as a QuoteUnavailable (available is False) with a reason (ADDRESS_NOT_SERVICEABLE, INTERSTATE_NOT_AVAILABLE, ...), never as an error. Branch on reason.code, not the message.
Address search
Complete an address as your customer types, or read one from a map pin, and give the quote the lga, state and place_id it needs. The addresses are Google's, in Nigeria. Call it from your server, as you do every call with your credentials.
import uuid
from reeds.shipping import AddressInputParam
# One session token per address being typed. Send it on every call while your customer
# types that address, and on the details call: the whole search then counts as one.
session = str(uuid.uuid4())
found = reeds.shipping.addresses.autocomplete(query="23 Allen Avenue", session_token=session)
for suggestion in found.suggestions: # at most 6, best first; empty when nothing matched
print(suggestion.place_id, suggestion.main_text, suggestion.secondary_text)
# The suggestion your customer picks, with the same token: this ends the search.
place = reeds.shipping.addresses.details(place_id=found.suggestions[0].place_id, session_token=session)
if place.lga is None:
raise ValueError("Ask your customer for the LGA: a quote needs it.")
pickup: AddressInputParam = {
"name": "Tola Fabrics",
"phone": "08051234567",
"address": place.formatted_address,
"lga": place.lga,
"state": place.state,
"place_id": place.place_id, # the spot is then exact
}
if place.area:
pickup["area"] = place.area
quote = reeds.shipping.quotes.create(pickup=pickup, dropoff=dropoff, package={"description": "2 pairs of sneakers", "quantity": 1, "weight_kg": 1.5})
precise is False for a whole area or road, whose point is only its middle: ask your customer for the street. For a map pin:
pin = reeds.shipping.addresses.reverse(latitude=6.6018, longitude=3.3515) # one search per pin
print(pin.formatted_address, pin.lga, pin.state, pin.place_id)
To deliver to the pin itself, send your pin's latitude and longitude in the quote, with this address as the text; to deliver to the address found, send its place_id. A pin outside Nigeria is an InvalidRequestError, and one with no address near it a NotFoundError.
Address search is free. Test mode has an allowance of 50 searches to start, which Reeds can raise; Live is unlimited unless Reeds sets a monthly limit for your account. Past it, a search raises PermissionDeniedError with the code ADDRESS_SEARCH_LIMIT_REACHED; if Reeds has switched address search off for your account, ADDRESS_SEARCH_DISABLED. A search you started can always finish. Google's rules apply to its addresses: show "Powered by Google" (or the Google logo) next to suggestions that aren't on a Google map, and show pins on a Google map. See the address search guide.
Deliveries
| Method | What it does |
|---|---|
deliveries.create(quote_id=..., reference=..., ready=..., ...) |
Books the delivery a quote priced, and charges it |
deliveries.list(status=..., reference=..., ids=..., created_from=..., created_to=..., limit=...) |
Deliveries, newest first (see pagination) |
deliveries.get(id) |
One delivery |
deliveries.update(id, reference=..., pickup_instructions=..., dropoff_instructions=..., metadata=...) |
Changes what can change |
deliveries.mark_ready(id) |
The parcel is packed: the pickup is scheduled from now |
deliveries.cancel(id, reason=...) |
Cancels and refunds in full, until the rider sets off |
deliveries.book(id) |
Books a priced delivery your team made in the dashboard |
deliveries.label(id) |
The shipping label, as a PDF |
Not packed yet? Book with ready=False. The delivery is charged and gets its tracking code, then waits with a MARK_READY action:
delivery = reeds.shipping.deliveries.create(quote_id=quote.id, ready=False)
# ...when the parcel is packed:
reeds.shipping.deliveries.mark_ready(delivery.id) # now "scheduled", with its pickup and delivery times
A booked delivery that isn't marked ready within 7 days is cancelled and refunded in full.
Delivery statuses
| Status | Meaning |
|---|---|
requires_action |
Waiting on you: required_actions says what (for example MARK_READY) |
scheduled |
Booked and charged, waiting for its pickup time and a rider |
rider_assigned |
A rider has the pickup |
picked_up |
Collected from the pickup address |
at_hub |
At a Reeds or partner hub |
in_transit |
Moving between hubs on an inter-state delivery |
out_for_delivery |
With the rider, heading to the receiver |
delivered |
Delivered. Final. |
failed_attempt |
The rider couldn't deliver. Reeds retries or contacts you. |
on_hold |
Paused by Reeds. hold_reason says why. |
cancelled |
Cancelled. Final. |
Reeds can add statuses: give every match on status a default case.
Labels
label = reeds.shipping.deliveries.label(delivery.id)
label.write_to(f"{delivery.id}.pdf") # or a directory, to save it as label.filename
label.data, label.content_type # the bytes, and "application/pdf"
Wallet and invoices
wallet = reeds.shipping.wallet.get()
print(wallet.balance) # kobo
# Live mode: start a Paystack top-up (₦50,000) and send the payer to the URL.
# wallet.funded carries the same reference when the money lands.
top_up = reeds.shipping.wallet.fund(amount=5_000_000)
print(top_up.payment_url, top_up.reference)
for entry in reeds.shipping.wallet.list_transactions():
print(entry.type, entry.amount, entry.description)
# Accounts billed monthly:
for invoice in reeds.shipping.invoices.list(status="issued"):
if invoice.id:
reeds.shipping.invoices.pdf(invoice.id).write_to(f"{invoice.number}.pdf")
Test mode and the sandbox
With test credentials, build the whole flow without real money or riders:
reeds.shipping.sandbox.fund_wallet(amount=1_000_000) # ₦10,000 in the test wallet, at once
reeds.shipping.sandbox.advance_delivery(delivery.id) # the next status, with its webhook
reeds.shipping.sandbox.advance_delivery(delivery.id, status="delivered") # straight to a status
Sandbox calls are refused before anything is sent when the client has live credentials.
Async
AsyncReeds has the same methods; await them. A list is awaited for one page, or walked with async for:
import asyncio
from reeds import AsyncReeds
from reeds.shipping import Quote
async def main() -> None:
async with AsyncReeds() as reeds:
quote = await reeds.shipping.quotes.create(pickup=pickup, dropoff=dropoff, package={"description": "Sneakers"})
if isinstance(quote, Quote):
delivery = await reeds.shipping.deliveries.create(quote_id=quote.id)
print(delivery.tracking_code)
page = await reeds.shipping.deliveries.list(limit=50) # await: one page
print(len(page.data), page.has_more)
async for delivery in reeds.shipping.deliveries.list(status="delivered"): # async for: every delivery
print(delivery.id)
asyncio.run(main())
Create one client and reuse it: it keeps one access token and one connection pool. async with (or await reeds.close()) closes the connections.
Webhooks
Reeds posts events to your endpoint as things happen. Verify each one before you trust it: pass the raw request body, because a body that was decoded and encoded again never matches the signature. The signing secret (whsec_test_... or whsec_live_...) is next to your endpoint in Dashboard → Developers. It isn't your client secret.
construct_event checks the signature and returns a typed event. Match on its class:
import os
from reeds import WebhookSignatureError, webhooks
from reeds.shipping import DeliveryStatusChangedEvent, WalletLowBalanceEvent
def handle(body: bytes, signature: str | None) -> int:
try:
event = webhooks.construct_event(body, signature, os.environ["REEDS_WEBHOOK_SECRET"])
except WebhookSignatureError as error:
print(error.message)
return 400
match event:
case DeliveryStatusChangedEvent():
print(event.data.delivery.id, event.data.previous_status, "->", event.data.delivery.status)
case WalletLowBalanceEvent():
print("Top up: the wallet is at", event.data.balance)
case _:
pass # Reeds adds event types over time: ignore the ones you don't use
return 200
webhooks verifies events without credentials; Reeds().webhooks does the same and can also send test events.
FastAPI
await request.body() is the raw body:
import os
from fastapi import FastAPI, Request, Response
from reeds import WebhookSignatureError, webhooks
app = FastAPI()
@app.post("/webhooks/reeds")
async def reeds_webhook(request: Request) -> Response:
try:
event = webhooks.construct_event(await request.body(), request.headers.get("reeds-signature"), os.environ["REEDS_WEBHOOK_SECRET"])
except WebhookSignatureError as error:
return Response(error.message, status_code=400)
print(event.id, event.type) # handle the event here
return Response(status_code=200)
Flask
request.get_data() is the raw body:
import os
from flask import Flask, request
from reeds import WebhookSignatureError, webhooks
app = Flask(__name__)
@app.post("/webhooks/reeds")
def reeds_webhook() -> tuple[str, int]:
try:
event = webhooks.construct_event(request.get_data(), request.headers.get("Reeds-Signature"), os.environ["REEDS_WEBHOOK_SECRET"])
except WebhookSignatureError as error:
return error.message, 400
print(event.id, event.type) # handle the event here
return "", 200
Django
request.body is the raw body. The view is exempt from CSRF: Reeds signs its requests instead. In your project the view goes in views.py and the route in urls.py:
import os
from django.http import HttpRequest, HttpResponse
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from reeds import WebhookSignatureError, webhooks
@csrf_exempt # Reeds signs its requests; there's no CSRF token to send
@require_POST
def reeds_webhook(request: HttpRequest) -> HttpResponse:
try:
event = webhooks.construct_event(request.body, request.headers.get("Reeds-Signature"), os.environ["REEDS_WEBHOOK_SECRET"])
except WebhookSignatureError as error:
return HttpResponse(error.message, status=400)
print(event.id, event.type) # handle the event here
return HttpResponse(status=200)
urlpatterns = [path("webhooks/reeds", reeds_webhook)]
| Event | When |
|---|---|
delivery.created |
A delivery was created: booked from a quote, or made in the dashboard |
delivery.requires_action |
An action was added to required_actions |
delivery.booked |
The delivery was priced and charged (or added to the month's invoice) |
delivery.status_changed |
Any status change |
delivery.schedule_changed |
The pickup day or delivery time moved |
delivery.expiring |
A delivery will be cancelled soon if nothing happens |
delivery.cancelled |
The delivery was cancelled |
wallet.funded |
A top-up landed and the wallet was credited |
wallet.low_balance |
The balance fell to or below your low-balance threshold |
invoice.issued |
A monthly invoice was issued |
invoice.paid |
An invoice was paid in full |
invoice.overdue |
An invoice passed its due date unpaid |
Each has a model in reeds.shipping (DeliveryBookedEvent, WalletFundedEvent, ...). An event type this version doesn't know yet arrives as an UnknownEvent, so a new type never breaks your endpoint.
Good to know:
- Duplicates: an event can arrive more than once. Skip
event.ids you've already processed. - Order: events can arrive out of order. Compare
event.data.delivery.updated_at. - Rotating a secret: pass both while you rotate, as
construct_event(body, signature, [new_secret, old_secret]). - Testing your receiver: send a signed test event to your endpoint:
reeds.webhooks.send_test(type="delivery.booked")
Lists and pagination
A list returns its first page, and iterating it walks every item, fetching pages as you go:
page = reeds.shipping.deliveries.list(status="delivered", limit=50)
page.data # up to 50, newest first
page.has_more # True when another page follows
page.next_page() # the next page, or None
for delivery in reeds.shipping.deliveries.list(status="delivered"):
... # every delivered delivery; pages are fetched as you go
With AsyncReeds, await the list for its first page and await page.next_page(), or async for over it for every item (async).
Errors
Every failure raises a ReedsError subclass. Branch on the class or on error.code, never on the message, which Reeds can reword.
from reeds import InvalidRequestError, PaymentRequiredError, ReedsError
try:
reeds.shipping.deliveries.create(quote_id=quote.id)
except PaymentRequiredError:
... # INSUFFICIENT_BALANCE: top up, then retry with the same idempotency_key
except InvalidRequestError as error:
print(error.fields) # [{"field": "dropoff.lga", "message": "Enter the LGA."}]
except ReedsError as error:
print(error.code, error.message, error.request_id, error.doc_url)
raise
| Class | Status | Typical codes |
|---|---|---|
AuthenticationError |
401, or wrong or missing credentials | TOKEN_EXPIRED, invalid_client, MISSING_CREDENTIALS |
PermissionDeniedError |
403 | INSUFFICIENT_SCOPE, IP_NOT_ALLOWED, TEST_MODE_ONLY, ACCOUNT_ON_HOLD, ADDRESS_SEARCH_LIMIT_REACHED, ADDRESS_SEARCH_DISABLED |
NotFoundError |
404 | NOT_FOUND |
ConflictError |
409 | ACTION_NOT_ALLOWED, QUOTE_ALREADY_USED, QUOTE_EXPIRED, CANCEL_NOT_ALLOWED |
InvalidRequestError |
400, 422 | INVALID_REQUEST (see fields), IDEMPOTENCY_KEY_REQUIRED |
PaymentRequiredError |
402 | INSUFFICIENT_BALANCE, CREDIT_LIMIT_REACHED |
RateLimitError |
429 | RATE_LIMITED (retry_after in seconds) |
ServiceUnavailableError |
503 | SERVICE_UNAVAILABLE (retry_after in seconds) |
APIError |
other 5xx, or a response the library doesn't recognise | INTERNAL_ERROR, UNEXPECTED_RESPONSE |
APIConnectionError, APIConnectionTimeoutError |
no response | CONNECTION_FAILED, TIMEOUT |
WebhookSignatureError |
a webhook that doesn't verify | SIGNATURE_MISMATCH, TIMESTAMP_OUTSIDE_TOLERANCE, RAW_BODY_REQUIRED |
Every error has:
status,codeandmessage;fields: what to fix, on validation errors;doc_url: the code's page on the developer hub;request_id: quote it to support;raw: the API's error object, including fields some codes add, such asdelivery_idonQUOTE_ALREADY_USED.
Retries, idempotency and timeouts
Retried (up to max_retries, default 2) |
Never retried |
|---|---|
| No response (network error, timeout) | 400, 401*, 402, 403, 404, 409, 422 |
429, 502, 503, 504, waiting for Retry-After (at most 30 s) |
|
500 on a GET |
|
409 REQUEST_IN_PROGRESS (the same key still running) |
* An expired token is refreshed once and the request sent again, automatically.
Waits back off from about half a second, up to 8 seconds. Every POST carries an Idempotency-Key, and a retry sends the same one, so Reeds acts on it once. To make your own retries safe too (after a crash, or a deploy), pass a key tied to your order:
reeds.shipping.deliveries.create(quote_id=quote.id, reference="ORDER-10045", idempotency_key="order-10045-delivery")
Every method also takes timeout (seconds), max_retries and extra_headers, for that request only:
reeds.shipping.deliveries.list(limit=10, timeout=10, max_retries=0)
Types
The models and parameter types follow the API's OpenAPI spec. Import them from reeds.shipping:
from reeds.shipping import Delivery, DeliveryStatus
def is_done(delivery: Delivery) -> bool:
final: list[DeliveryStatus] = ["delivered", "cancelled"]
return delivery.status in final
Parameters are TypedDicts, so a dict you build ahead is checked too:
from reeds.shipping import AddressInputParam
pickup: AddressInputParam = {"name": "Tola Fabrics", "phone": "08051234567", "address": "23 Allen Avenue", "lga": "Ikeja", "state": "Lagos"}
The types accept values Reeds adds later: an enum such as DeliveryStatus is its known values or any string, so a new status type-checks and parses. Fields Reeds adds are kept on the model:
print(delivery.to_dict()) # the API's JSON, including fields this version doesn't know
delivery.model_extra # just the fields this version doesn't know
An object that doesn't fit its model (a field missing, a value of another type) never raises: the library keeps what came, so a call that charged never looks like it failed.
Configuration
import httpx2
from reeds import Reeds
reeds = Reeds(
client_id="rci_test_...", # default: REEDS_CLIENT_ID
client_secret="rcs_test_...", # default: REEDS_CLIENT_SECRET
base_url="https://api.getreeds.co", # the default
timeout=60, # seconds per attempt
max_retries=2,
default_headers={"X-Your-Header": "value"},
http_client=httpx2.Client(proxy="http://proxy.internal:8080"), # proxies, custom TLS
)
with Reeds() as reeds: # closes its connections at the end
reeds.shipping.coverage.get()
AsyncReeds takes the same options, with an httpx2.AsyncClient as http_client. A client you pass in stays open: close it yourself.
Versioning
The library follows semantic versioning. Until 1.0.0, a minor version may change something you use: read the changelog before upgrading.
Within an API version (v1), Reeds adds fields, statuses and event types without notice. The library keeps them rather than dropping them, and its types allow them.
Every release is built by GitHub Actions and published to PyPI with trusted publishing, with no long-lived token.
Support
- Docs and guides: developer.getreeds.co.
- Help with a call: email support@getreeds.co with the
request_idfrom the error (or theX-Request-Idheader). Every call is also listed in Dashboard → Developers → API requests for 30 days. - A bug in the library: open an issue.
- A security problem: email support@getreeds.co rather than opening an issue.
License
MIT © Reeds Merchants Technologies Ltd.
Metadata
Release files for reeds-sdk 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| reeds_sdk-0.2.0.tar.gz | 108.5 kB | With provenance | Details for reeds_sdk-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| reeds_sdk-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for reeds_sdk-0.2.0-py3-none-any.whl |
Total release size: 154.2 kB
Release files / reeds_sdk-0.2.0.tar.gz
| Download URL | reeds_sdk-0.2.0.tar.gz |
|---|---|
| Size | 108.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
606c9e733d2153815f95ff885994a01dc348c05f186cdcb1ecf6e68421baff3c
|
|
BLAKE2b-256 checksum How to use checksums |
a4d160cb8ea2e85cbe59ee7a333da086bf42a76f289d0a40994a641ef75cd24a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logRelease files / reeds_sdk-0.2.0-py3-none-any.whl
| Download URL | reeds_sdk-0.2.0-py3-none-any.whl |
|---|---|
| Size | 45.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9a539d55988173e71c687b8c545b1032dbe8e71edbdf524121e28eb2602719b4
|
|
BLAKE2b-256 checksum How to use checksums |
9041b8daa807a940fe8af8ba232e72788e2e40fdd98cb7453c6f2c67b0709fd3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logSecurity for reeds-sdk 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for reeds_sdk-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.