rayacash
El SDK oficial de Python para la API de Raya Cash. Te deja cobrar por transferencia con un CVU exclusivo por pago, mandar plata a cualquier CBU, CVU o alias, consultar tu saldo, conciliar tus depósitos y verificar los webhooks, con reintentos seguros e idempotencia incluidos.
import rayacash
client = rayacash.RayaClient() # lee tu clave secreta de RAYA_SECRET_KEY
payment = client.payments.create(amount=15000, description="Pedido #1042")
print(payment.bank_transfer.cvu, payment.checkout_url)
Versión 1.0.1. Requiere Python 3.9 o más nuevo. Los cambios de cada versión están en CHANGELOG.md.
Contenido
- Antes de empezar
- Instalación
- Inicio rápido
- Estilo global
- Async
- Montos
- Idempotencia
- Reintentos y timeouts
- Paginación
- Errores
- Webhooks
- Tests de tu integración
- Configuración avanzada
- Seguridad
- Versionado y compatibilidad
- Desarrollo
Antes de empezar
- Tu cuenta de Raya con la identidad verificada (si no,
API_KEY_ACCOUNT_LOCKED) y la API habilitada (si no,API_NOT_ENABLED: escribinos a soporte@raya.cash). - Tu clave secreta (
raya_sk_live_…), de Developers › API Keys. La vas a ver completa una sola vez: guardala en una variable de entorno de tu servidor. - Para cobrar: tu negocio configurado en Configuración › Negocio. Sin eso, crear un pago da
MERCHANT_NOT_CONFIGURED. - Para los webhooks: un destino en Developers › Webhooks y su secreto (
whsec_…).
Instalación
pip install rayacash
# o
uv add rayacash
La única dependencia es httpx (más typing_extensions).
Inicio rápido
import os
from decimal import Decimal
import rayacash
client = rayacash.RayaClient(os.environ["RAYA_SECRET_KEY"])
# Tu saldo disponible, en pesos.
balance = client.balance.retrieve()
print(balance.available)
# Cobrar: un pago con un CVU y un alias exclusivos.
payment = client.payments.create(
amount=15000,
description="Pedido #1042",
external_reference="pedido-1042",
return_url="https://tienda.example.com/pedidos/1042",
metadata={"cliente_id": "8812"},
idempotency_key=order.payment_idempotency_key, # un UUID v4 guardado con el pedido
)
# Mostrá payment.bank_transfer.cvu y payment.bank_transfer.alias, o mandá a quien paga a:
print(payment.checkout_url)
# Consultar un pago (por ejemplo, cuando quien paga vuelve a return_url).
payment = client.payments.retrieve(payment.id)
if payment.status == "succeeded":
... # entregá el pedido
# Reembolsar un pago (total, hasta 30 días después de pagado).
payment = client.payments.refund(payment.id, idempotency_key=refund.idempotency_key)
# Enviar una transferencia a un CBU, CVU o alias. La idempotency_key es obligatoria.
transfer = client.transfers.create(
amount=Decimal("25000.50"),
destination="0000123380736153041182",
idempotency_key=payout.idempotency_key, # guardala ANTES de llamar
)
print(transfer.status, transfer.counterparty.name) # "pending" (otro banco) o "completed" (Raya)
# Tus depósitos de un día, página por página.
for deposit in client.transfers.list(
direction="inbound",
created_gte="2026-10-23T00:00:00-03:00",
created_lt="2026-10-24T00:00:00-03:00",
limit=100,
).auto_paging_iter():
print(deposit.id, deposit.net_amount)
Cada respuesta es un objeto del SDK (Balance, Transfer, Payment, Event o ListObject) que también es un dict: payment.status y payment["status"] dan lo mismo, y json.dumps(payment) anda. Los valores vienen tal como los manda la API: montos int o float y fechas en string ISO 8601 ("2026-10-24T14:00:00Z"). Si la API suma un campo nuevo, lo ves igual (payment["campo_nuevo"]).
Cada objeto trae last_response, con el request_id (req_…) que nos tenés que mandar si escribís a soporte:
print(payment.last_response.request_id)
Estilo global
Si venís de stripe-python, también podés configurar una clave para todo el proceso y llamar a las clases:
import os
import rayacash
rayacash.api_key = os.environ["RAYA_SECRET_KEY"] # si no la asignás, se lee de RAYA_SECRET_KEY
balance = rayacash.Balance.retrieve()
payment = rayacash.Payment.create(amount=15000, description="Pedido #1042")
payment = rayacash.Payment.retrieve(payment.id)
refunded = payment.refund() # o rayacash.Payment.refund(payment.id)
transfer = rayacash.Transfer.create(
amount=25000, destination="proveedor.ejemplo", idempotency_key=payout.idempotency_key
)
page = rayacash.Transfer.list(direction="inbound")
Los dos estilos usan el mismo núcleo y se comportan igual. Preferí RayaClient si tu proceso usa más de una clave o si querés configurar cada cliente por separado. payment.refund() usa el cliente del que vino el pago: si lo trajiste con un RayaClient, usa ese cliente; si no, la configuración global.
Async
Cada método tiene su versión *_async, para asyncio (FastAPI, Starlette, aiohttp…):
import rayacash
client = rayacash.RayaClient()
async def conciliar() -> None:
balance = await client.balance.retrieve_async()
payment = await client.payments.create_async(amount=15000)
page = await client.transfers.list_async(direction="inbound")
async for transfer in page.auto_paging_iter():
print(transfer.id)
await client.aclose() # o usá `async with rayacash.RayaClient() as client:`
Montos
- La API trabaja en pesos (no en centavos), con hasta 2 decimales.
currencyes siempre"ARS". - Para mandar un monto podés pasar
int,Decimal,floato unstrdecimal:15000,Decimal("15000.50"),15000.5o"15000.50". Viaja como número JSON (nunca como string). - El SDK nunca redondea. Si mandás más de 2 decimales (por ejemplo, un float con ruido como
0.1 + 0.2), la API respondePARAMETER_INVALIDy no se hace nada. Si tus montos salen de cuentas, usáDecimaly redondeá vos conquantize(Decimal("0.01")). - Los montos que recibís (
amount,fee,net_amount,available…) llegan como los manda la API:intsi son redondos yfloatsi tienen centavos. Para sumar o comparar con exactitud (por ejemplo, contra unDecimalFieldde Django), convertilos así:
from decimal import Decimal
total = sum(Decimal(str(t.net_amount)) for t in page) # exacto: Decimal("48169.59")
Idempotencia
Con una Idempotency-Key podés reintentar un POST sin miedo a que se haga dos veces: si ya lo procesamos, te devolvemos el mismo resultado.
transfers.createexigeidempotency_key. Generá un UUID v4 (str(uuid.uuid4())) para cada transferencia, guardalo con la operación en tu base antes de llamar y reusalo en cada reintento. Así, si tu proceso se corta después de mandar la request, el reintento no transfiere dos veces.- En
payments.createypayments.refundes opcional pero recomendada. Si no la pasás, el SDK genera una que protege solo los reintentos de esa llamada. - El SDK reintenta siempre con la misma clave. La que usó queda en
obj.last_response.idempotency_keyy enerror.idempotency_key, para que puedas reintentar más tarde. - Guardamos cada clave 24 horas, para toda tu cuenta. Usar la misma clave con otros parámetros da
IdempotencyErrorconcode == "IDEMPOTENCY_KEY_REUSED". - Si una transferencia terminó
failed, para intentarla de nuevo usá una clave nueva: con la misma te devolvemos la respuesta original.
import uuid
key = str(uuid.uuid4())
payout.save_idempotency_key(key) # primero guardala
try:
transfer = client.transfers.create(
amount=25000, destination="proveedor.ejemplo", idempotency_key=key
)
except rayacash.APIConnectionError:
... # no sabemos si salió: reintentá más tarde con la MISMA key (nunca se hace dos veces)
Reintentos y timeouts
El SDK reintenta solo lo que es seguro reintentar, siempre con la misma Idempotency-Key:
| Respuesta | ¿Reintenta? | Espera |
|---|---|---|
| Sin respuesta (corte de conexión, timeout) | Sí | Exponencial con jitter (0,5 s, 1 s, 2 s… hasta 8 s) |
409 IDEMPOTENCY_KEY_IN_USE |
Sí | Lo que pide Retry-After |
429 RATE_LIMITED |
Sí | Retry-After |
408, 500, 502, 503, 504 |
Sí | Retry-After o exponencial |
503 API_UNAVAILABLE (mantenimiento) |
No | — |
Cualquier otro 4xx |
No: no se hizo nada | — |
- Por defecto, 2 reintentos (3 intentos en total). Cambialo con
RayaClient(max_network_retries=…)o por llamada conoptions={"max_network_retries": 0}. - Si el servidor pide esperar más de 60 s (
Retry-After), el SDK no se queda esperando: levanta el error conerror.retry_afterpara que lo reprogrames. - Timeout: 65 s por intento (10 s para conectar). Crear un pago puede tardar hasta 40 s si damos de alta un CVU nuevo y el servidor corta a los 60 s, así que no lo bajes de 60. Cambialo con
RayaClient(timeout=…)o por llamada conoptions={"timeout": 90}.
client = rayacash.RayaClient(max_network_retries=4, timeout=90)
balance = client.balance.retrieve(options={"timeout": 10, "max_network_retries": 0})
Paginación
transfers.list() devuelve una página (ListObject) de la más nueva a la más vieja. Iterarla recorre esa página; auto_paging_iter() recorre todas, pidiendo las siguientes con los mismos filtros:
from datetime import datetime, timedelta, timezone
argentina = timezone(timedelta(hours=-3))
page = client.transfers.list(
direction="inbound", # o "outbound"
status="completed", # pending, completed, failed o reversed
created_gte=datetime(2026, 10, 1, tzinfo=argentina), # datetime con zona o string ISO 8601
created_lt=datetime(2026, 11, 1, tzinfo=argentina), # rango semiabierto: los meses no se pisan
limit=100, # de 1 a 100 (por defecto, 20)
)
for transfer in page.auto_paging_iter():
print(transfer.id, transfer.amount)
# A mano: page.has_more, page.next_cursor y page.next_page().
Los cobros de tus pagos no aparecen en Transferencias: se siguen en cada Payment y con los webhooks payment.*.
Errores
Todos los errores heredan de rayacash.RayaError. Decidí con error.code (estable), nunca con error.message (texto en castellano para personas, puede cambiar). Si llega un código que el SDK no conoce, la clase sale del código HTTP.
| Clase | HTTP | Códigos | Qué hacer |
|---|---|---|---|
InvalidRequestError |
400, 405, 413 y los demás 4xx |
PARAMETER_MISSING, PARAMETER_INVALID, PARAMETER_UNKNOWN, INVALID_JSON… |
Corregí la request: error.param dice qué parámetro falló. |
NotFoundError (subclase de la anterior) |
404 | PAYMENT_NOT_FOUND, TRANSFER_NOT_FOUND, NOT_FOUND |
No existe o no es de tu cuenta. |
AuthenticationError |
401 | API_KEY_MISSING, API_KEY_INVALID, API_KEY_EXPIRED… |
Revisá la clave secreta. |
PermissionDeniedError |
403 | API_KEY_ACCOUNT_LOCKED, API_NOT_ENABLED, MERCHANT_NOT_CONFIGURED, TRANSFERS_BLOCKED |
La cuenta no puede hacer esto todavía. |
IdempotencyError |
409 | IDEMPOTENCY_KEY_REUSED, IDEMPOTENCY_KEY_IN_USE |
Una clave por operación; si está en uso, reintentá más tarde con la misma. |
ConflictError |
409 | PAYMENT_NOT_REFUNDABLE |
Mirá error.data["status"] y error.data.get("reason"). |
UnprocessableEntityError |
422 | INSUFFICIENT_BALANCE, LIMIT_EXCEEDED, DESTINATION_NOT_FOUND, DESTINATION_INACTIVE, DESTINATION_NOT_SUPPORTED, REFUND_FAILED |
No se arregla reintentando: mostráselo a la persona. |
RateLimitError |
429 | RATE_LIMITED |
Esperá error.retry_after segundos. |
APIError |
5xx y 408 | INTERNAL_ERROR, BANK_UNAVAILABLE, CVU_UNAVAILABLE, API_UNAVAILABLE, TEMPORARILY_UNAVAILABLE |
Reintentá más tarde, en los POST con la misma idempotency_key. |
APIConnectionError |
— | NETWORK_ERROR, TIMEOUT |
No llegó respuesta: reintentá con la misma idempotency_key. |
Cada error trae code, message, data, http_status, request_id, idempotency_key, retry_after, headers y el body crudo (http_body, json_body).
try:
transfer = client.transfers.create(
amount=25000, destination="proveedor.ejemplo", idempotency_key=key
)
except rayacash.UnprocessableEntityError as error:
if error.code == "INSUFFICIENT_BALANCE":
print("No te alcanza el saldo.")
else:
print(error.message)
except rayacash.APIError as error:
if error.code == "BANK_UNAVAILABLE":
... # no se descontó nada: reintentá más tarde con la misma key
except rayacash.RayaError as error:
print(f"Falló ({error.code}). Request: {error.request_id}")
Un reembolso que la red todavía no confirmó sale como APIError con code == "BANK_UNAVAILABLE": el pago sigue succeeded y la devolución nunca se manda dos veces. Esperá el webhook payment.refunded o reintentá más tarde con la misma clave.
Webhooks
Te avisamos en tu servidor apenas pasa algo: payment.succeeded cuando llega la transferencia de un pago, transfer.received cuando te depositan, etc. Antes de procesar un evento, verificá la firma:
import os
from flask import Flask, request
import rayacash
app = Flask(__name__)
@app.post("/webhooks/raya")
def raya_webhook():
try:
event = rayacash.Webhook.construct_event(
request.get_data(), # el body crudo, sin parsear
request.headers.get("Raya-Signature"),
os.environ["RAYA_WEBHOOK_SECRET"], # whsec_…
)
except rayacash.SignatureVerificationError:
return "Firma inválida", 400
if already_processed(event.id): # el mismo evento puede llegar más de una vez
return "", 200
if event.type == "payment.succeeded":
payment = event.data.object
fulfill_order(payment.external_reference)
elif event.type == "transfer.received":
record_deposit(event.data.object)
return "", 200
- Usá el body crudo, tal como llegó. Si tu framework ya parseó el JSON, volver a serializarlo cambia el body y la firma no coincide. En Flask,
request.get_data(); en Django,request.body; en FastAPI,await request.body(). Hay ejemplos completos enexamples/. - Respondé
2xxen menos de 15 segundos. Si tenés que hacer algo lento, guardá el evento, respondé y procesalo después. - El mismo evento puede llegar más de una vez y desordenado: guardá los
event.idque procesaste y, si necesitás el estado actual, consultá el objeto con la API. event.data.objectes elTransfero elPaymenttal como estaba cuando ocurrió el evento.- «Enviar evento de prueba» (en Developers › Webhooks) manda un
webhook.test: respondé2xxe ignoralo. - La firma tolera 5 minutos de diferencia con tu reloj (
tolerance=300). Mientras rotás el secreto llegan dos firmas: alcanza con que coincida una. - Con
client.construct_event(...)el evento queda ligado a ese cliente:event.data.object.refund()usa su clave.
Tests de tu integración
Para probar tu código sin llamar a la API, usá un http_client falso de httpx: no sale nada a la red.
import httpx
import rayacash
def handler(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/v1/balance"
return httpx.Response(200, json={"object": "balance", "available": 1500, "currency": "ARS"})
client = rayacash.RayaClient(
"raya_sk_live_test",
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
)
assert client.balance.retrieve().available == 1500
Para probar tu endpoint de webhooks, firmá el body con rayacash.WebhookSignature.generate_header(body, secret) y mandalo en el header Raya-Signature.
Configuración avanzada
import httpx
import rayacash
client = rayacash.RayaClient(
api_key="raya_sk_live_…",
api_base="https://api.raya.cash", # https:// (o http://localhost para desarrollo)
timeout=65, # segundos por intento, o un httpx.Timeout
max_network_retries=2,
http_client=httpx.Client(proxy="http://proxy.interno:3128"), # el tuyo: no lo cerramos
async_http_client=httpx.AsyncClient(proxy="http://proxy.interno:3128"),
app_info={"name": "mi-plugin", "version": "1.2.0", "url": "https://plugin.example"},
)
- Proxies y certificados: pasá tus propios
httpx.Client/httpx.AsyncClient. httpx también respetaHTTPS_PROXY. app_info: identifica tu plugin en elUser-Agent(rayacash-python/1.0.1 … mi-plugin/1.2.0), que ves en Developers › Logs. En el estilo global:rayacash.set_app_info("mi-plugin", version="1.2.0").- Logging: el SDK usa el logger
rayacash. ConRAYA_LOG=info(orayacash.log = "info") ves cada request, su status, surequest_idy los reintentos en stderr; condebug, también la query y la idempotency key. Vale para todo el proceso, también para cadaRayaClient. Nunca logueamos la clave ni los bodies. - Cerrar:
client.close()/await client.aclose(), o usáwith/async with. - Endpoints nuevos: si la API suma un endpoint antes que el SDK, usá
client.raw_request("get", "/v1/…", params=…, body=…): tiene la misma autenticación, reintentos y errores. La query va enparams, nunca en la ruta. - Estilo global:
rayacash.api_base,rayacash.timeout,rayacash.max_network_retries,rayacash.http_clientyrayacash.async_http_client.
Seguridad
- La clave secreta va solo en tu servidor, leída de una variable de entorno. Nunca en el navegador, en una app mobile ni en el repositorio. Para cobrar desde el navegador está Raya.js, con la clave pública.
- El SDK rechaza la clave pública (
raya_pk_…) sin mandar nada, nunca la muestra completa (repr(client)la enmascara) y nunca sigue redirects. - Si la clave se filtró, rotala en Developers › API Keys con vencimiento «Ahora»: la anterior deja de funcionar al instante.
- Las requests que hacés con la clave secreta quedan 30 días en Developers › Logs.
Versionado y compatibilidad
- Semver: los cambios incompatibles solo llegan en una versión mayor (
2.0.0). Las menores (1.1.0) suman cosas sin romper nada y las de parche (1.0.1) corrigen errores. Todo queda anotado en el CHANGELOG. - La API puede sumar campos, valores de enums, eventos, códigos de error y endpoints. El SDK los deja pasar: tu código tiene que ignorar lo que no conoce.
- Python 3.9 a 3.14. Viene con tipos (
py.typed): mypy y pyright conocen cada objeto y sus campos.
Desarrollo
uv sync # entorno con las dependencias de desarrollo
uv run pytest # tests (nunca llaman a la API: usan httpx.MockTransport)
uv run ruff check . && uv run ruff format --check .
uv run mypy && uv run pyright
uv build # wheel y sdist en dist/
Para publicar una versión: actualizá src/rayacash/_version.py y el CHANGELOG, y creá un release vX.Y.Z en GitHub. El workflow release.yml corre los tests y publica en PyPI con Trusted Publishing.
Referencia de la API: https://raya.cash/api · Soporte: soporte@raya.cash
Metadata
Release files for rayacash 1.0.1
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 |
|---|---|---|---|---|
| rayacash-1.0.1.tar.gz | 70.1 kB | With provenance | Details for rayacash-1.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| rayacash-1.0.1-py3-none-any.whl | Python 3 | none | any | Details for rayacash-1.0.1-py3-none-any.whl |
Total release size: 118.4 kB
Release files / rayacash-1.0.1.tar.gz
| Download URL | rayacash-1.0.1.tar.gz |
|---|---|
| Size | 70.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6f05a6b93cf850558b53b1d63eb4101ff2d3277a82b04b6759cb29ba9566ac30
|
|
BLAKE2b-256 checksum How to use checksums |
7eeed7ac4df020374ef2731d545f2c83f5234583e855a114c830627d06b87d09
|
| 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 2, 2026.
Transparency logRelease files / rayacash-1.0.1-py3-none-any.whl
| Download URL | rayacash-1.0.1-py3-none-any.whl |
|---|---|
| Size | 48.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e24da27d1e354d3fea9ca8b12c203ade9bce643ec7f35d2b4467c5fb6d5b38ba
|
|
BLAKE2b-256 checksum How to use checksums |
d97838d7f98a5903113f2a84b73ced51ce675f00b302521f276d8031087044c8
|
| 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 2, 2026.
Transparency logSecurity for rayacash 1.0.1
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
All 2 files in this release have been uploaded with provenance metadata.
Attestation for rayacash-1.0.1.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 2, 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.