Skip to main content

payriff

Python client for the Payriff payment gateway. Sync and async, typed throughout, with httpx as the only dependency.

pip install payriff

Quick start

from payriff import PayriffClient

client = PayriffClient("your-secret-key")

order = client.create_order(
    amount=25.50,
    description="Order 1024",
    callback_url="https://shop.example/payriff/callback",
)
print(order.payment_url)

Send the customer to order.payment_url. Payriff posts to your callback_url as the status changes.

A whole checkout

The shape of a real integration, here with Flask. Two routes: one to start the payment, one to receive the result.

from flask import Flask, jsonify, redirect, request

from payriff import GatewayError, PayriffClient

app = Flask(__name__)
client = PayriffClient.from_env()


@app.post("/checkout/<cart_id>")
def checkout(cart_id):
    cart = load_cart(cart_id)
    try:
        order = client.create_order(
            amount=cart.total,
            description=f"Order {cart_id}",
            callback_url="https://shop.example/payriff/callback",
        )
    except GatewayError as exc:
        app.logger.warning("payriff refused the order: %s %s", exc.code, exc.message)
        return jsonify(error="could not start the payment"), 502

    cart.payriff_order_id = order.order_id
    cart.save()
    return redirect(order.payment_url)


@app.post("/payriff/callback")
def callback():
    event = client.parse_callback(request.get_json(force=True))

    # The callback is unsigned, so nothing here is trusted until the API agrees.
    order = client.get_order(event.order_id)
    cart = load_cart_by_order(event.order_id)

    if order.settled:
        cart.mark_paid()
    elif order.payment_status in {"DECLINED", "CANCELED"}:
        cart.mark_failed(order.payment_status)

    return "", 200

Two details worth copying. The callback handler decides on order, the value it fetched, never on event, the value it was sent. And it answers 200 whatever the outcome, because a non-2xx tells Payriff to deliver the same event again.

Callbacks can arrive more than once for one order, so make mark_paid idempotent.

Two things that catch people out

A success code doesn't mean the payment worked

The code field tells you whether Payriff accepted the call. Whether money actually moved is a separate field, paymentStatus. auto_pay is where this hurts most: a declined card still comes back as 00000.

charge = client.auto_pay(card_uuid, 10.00, "Subscription")

charge.ok              # True
charge.payment_status  # "CANCELED"
charge.settled         # False

Gate your fulfilment on settled.

A hold is different again. A genuine pre-authorisation reports PREAUTH_APPROVED, and that is the only status where money is reserved rather than taken. settled stays False for it and authorized turns True.

hold = client.get_order(order_id)
hold.authorized  # True, the funds are reserved
hold.settled     # False, nothing has moved yet

Watch out for accounts without pre-authorisation enabled. There, an order sent with operation=PRE_AUTH is charged like an ordinary sale and comes back APPROVED, which settled correctly reports as paid. Check authorized, not the operation you asked for.

Callbacks aren't signed

There is no HMAC and no shared secret in the body, so there's nothing to verify. Anyone who learns your callback URL can post to it. Treat the callback as a nudge to go ask the API what happened:

callback = client.parse_callback(request.json)
order = client.get_order(callback.order_id)

if order.settled:
    fulfil(callback.order_id)

parse_callback reads the body and nothing more. The get_order call is what makes it safe.

Configuration

client = PayriffClient(
    "your-secret-key",
    merchant_id="ES100000",
    currency="AZN",
    language="EN",
    callback_url="https://shop.example/payriff/callback",
)

PayriffClient.from_env() reads PAYRIFF_SECRET_KEY, PAYRIFF_MERCHANT_ID, PAYRIFF_BASE_URL and PAYRIFF_CALLBACK_URL instead.

The secret goes into the Authorization header raw, with no Bearer prefix. The client handles that for you.

Async

Every method has an awaitable twin:

from payriff import AsyncPayriffClient

async with AsyncPayriffClient("your-secret-key") as client:
    order = await client.create_order(25.50, "Order 1024")
    status = await client.get_order(order.order_id)

Methods

Method Endpoint
create_order POST /api/v3/orders
reserve POST /api/v3/orders with operation=PRE_AUTH
get_order GET /api/v3/orders/{id}
complete POST /api/v3/complete
refund POST /api/v3/refund
auto_pay POST /api/v3/autoPay
save_card, refund_card_save the two step card save flow
transfer POST /api/v3/payout
create_invoice POST /api/v2/invoices
get_invoice POST /api/v2/get-invoice

A couple of things worth knowing. The docs describe order information as a POST, but the API answers that with a 405 and only takes GET, which is what this does. And /api/v3/payout moves money between Payriff merchant wallets, so transfer seemed the honest name for it; if you were hoping for a bank payout, this isn't it.

The v2 invoice endpoints want merchant set and refuse the call without it. The v3 order endpoints do not care, so set merchant_id on the client if you touch invoices.

Bulk invoice and bulk payout are dashboard features driven by spreadsheet upload. There is no API behind them, so there's nothing here for them either.

For anything else, go straight at it:

client.request("POST", "/api/v3/directPay", {...})

Pre-authorisation

reserve puts a hold on the card instead of charging it. You have 30 days to capture with complete, after which the hold expires on its own. Capture less than you held and Payriff releases the difference.

hold = client.reserve(100.00, "Hotel booking", three_ds=True)
client.complete(hold.order_id, 80.00)

Saving a card

To verify a card, Payriff charges 0.01 AZN and expects you to hand it straight back. Skipping the refund leaves the customer out of pocket, so treat it as step two rather than tidying up:

verification = client.save_card(callback_url="https://shop.example/payriff/callback")
# the customer completes verification.payment_url, then
client.refund_card_save(verification.order_id)

The cardUuid you need for auto_pay arrives in the callback.

Responses

Branch on code rather than the HTTP status. The interesting fields live under payload, and Response reads through to them:

order.code            # "00000"
order.code_name       # "SUCCESS"
order.ok              # the call was accepted
order.settled         # money actually moved
order.authorized      # funds held by a pre-authorisation, not yet captured
order.operation_type  # "PURCHASE", "PRE_AUTH", ...
order.order_id, order.session_id, order.transaction_id, order.payment_url
order.payment_status  # "PAID", "CANCELED", ...
order.transactions    # per attempt: card mask, RRN, channel
order.payload         # raw payload, occasionally a plain string such as "APPROVED"
order.raw             # untouched body

Enums

Values are case sensitive and always go over the wire as strings.

from payriff import Currency, Language, PaymentStatus

client.create_order(10, "x", currency=Currency.USD, language=Language.AZ)

if order.payment_status == PaymentStatus.APPROVED:
    ...
Enum Values
Operation PURCHASE, PRE_AUTH, COMPLETE, REFUND, REVERSE
Language AZ, EN, RU, AR
Currency AZN, USD, EUR, PKR, AED, SAR
PaymentType ONETIME, DAILY, WEEKLY, MONTHLY, ANNUALLY
InstallmentProduct BIRKART, ALBALI, BOLKART, TAMKART
InvoiceStatus PENDING, ERROR, EXPIRED, PARTIAL, COMPLETE, CASH
PaymentStatus CREATED, APPROVED, CANCELED, DECLINED, REFUNDED, PREAUTH_APPROVED, EXPIRED, REVERSE, PARTIAL_REFUND, PARTIAL, ACCEPTED, REFUND_IN_PROGRESS, CASH, PENDING, PREAUTH_EXPIRED
ResultCode SUCCESS, WARNING, ERROR, INVALID_PARAMETERS, UNAUTHORIZED, TOKEN_NOT_PRESENT, INVALID_TOKEN, INVALID_ORIGIN, CHECKING
GatewayResult 00, APPROVED, PREAUTH-APPROVED

Members hash by value, so a raw string off the wire and an enum member reach the same dict entry. CURRENCY_NUMERIC and PAYMENT_STATUS_CODES give you the ISO 4217 and internal numeric codes.

PaymentStatus carries two extras, PAID and COMPLETED. Payriff's enum reference leaves them out, but the order information and autoPay examples both return them, so they are in here. SETTLED_PAYMENT_STATUSES and HELD_PAYMENT_STATUSES are the sets behind settled and authorized.

Errors

GatewayError means Payriff refused the call: any result code other than 00000 or 01000, or a 4xx that still carried an envelope. It gives you code, message, internal_message and response_id.

TransportError covers the rest, so network failures and responses that either weren't JSON or carried no envelope at all.

Both subclass PayriffError if you want to catch broadly.

from payriff import GatewayError, PayriffError, TransportError

try:
    order = client.create_order(25.50, "Order 1024")
except GatewayError as exc:
    # Payriff answered and said no. exc.code tells you why.
    log.warning("refused: %s %s", exc.code, exc.message)
except TransportError as exc:
    # Nothing came back. Safe to retry.
    log.error("payriff unreachable: %s", exc)

Retry TransportError if you like, but not GatewayError: the gateway already made up its mind and the same call will be refused again.

The 01000 code is a bit odd. On a 2xx it's a warning and comes back as a normal response. On a 4xx it's a refusal and raises, which is how Payriff answers an unknown application key.

Testing

Payriff runs a sandbox. Point PAYRIFF_BASE_URL at it, use a sandbox key, and pay with the published test cards:

Brand Number Expiry CVV OTP
Visa 4000007546012078 04/29 893 123456
Mastercard 5100007346013947 04/29 783 123456

tests/test_sandbox.py runs when PAYRIFF_SECRET_KEY is set and skips when it isn't, so a checkout without a key still passes.

What has been checked against the live gateway

Payriff's docs turned out to disagree with the API in a few places, so the flows below were run against a real account and the parsing is built from the recorded bodies.

Verified Inferred from the docs only
create order, and the CREATED to PENDING to APPROVED or DECLINED lifecycle complete, a capture
that a pre-auth order settles outright on an account without pre-auth auto pay
order lookup, which is a GET despite the docs saying POST card save and its refund
refund, which answers with a null payload, on both a purchase and a pre-auth invoice create and lookup
the callback envelope, for a declined purchase wallet transfer
result codes 15400, 15000 and 01000, and the 4xx behaviour

Pre-authorisation, autopay and invoicing are not switched on for the account used, so those paths could not be exercised. Treat the right hand column as untested and please report anything that looks wrong.

Licence

MIT. Not affiliated with Payriff.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

payriff-0.1.0.tar.gz (57.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

payriff-0.1.0-py3-none-any.whl (17.9 kB view details)

Uploaded Python 3

File details

Details for the file payriff-0.1.0.tar.gz.

File metadata

  • Download URL: payriff-0.1.0.tar.gz
  • Upload date:
  • Size: 57.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for payriff-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8668f50931124bfafc436d0d206eaa5af882e7bf62ed68e0fa6f58cb68dc6442
MD5 8c5ced38c4c28c069614dcbe62a0fcf6
BLAKE2b-256 6f022ddee3f365b2ada85f9f4091ca8457eea2dbf9df7c5313bc75561c57d66a

See more details on using hashes here.

File details

Details for the file payriff-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: payriff-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 17.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for payriff-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 835a371efb1ba7826ad7686024f73ed6f7999efad6e6ce69acb7d60f6205c4fe
MD5 2a27aac6fa810386e20439d2aab08559
BLAKE2b-256 1710f9d76a90cc3f899ea5f551bd05306568f1f0ce9680ae8c42628cc00c81fb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page