Contents
Developers SDKs
Python SDK
The REST API for Python 3.9 and newer: an idempotency key on every POST, bounded retries, typed exceptions and webhook verification. The standard library only, no dependencies. Writing TypeScript or PHP? Use the TypeScript or PHP SDK.
Install
Configuration
One client per process, made with your secret key. A ps_test_ key reaches the sandbox, a ps_live_ key production. Keep the key on your server: never in a page or an app.
import os
from payshen import Payshen
payshen = Payshen(
os.environ["PAYSHEN_API_KEY"],
timeout=20.0, # seconds per attempt
max_retries=3,
base_url="https://www.payshen.com",
)base_url must be https; plain http is accepted for localhost only, and anything else is refused when the client is made.
Your first payment
import os
from payshen import Payshen
payshen = Payshen(os.environ["PAYSHEN_API_KEY"]) # ps_test_... in the sandbox
payment = payshen.payments.create(
{
"amount": 5000, # minor units: EUR 50.00
"currency": "EUR",
"country": "DE",
"reference": "order-1024",
"capture_method": "manual", # hold now, capture when you ship
"return_url": "https://yourshop.com/checkout/return",
},
idempotency_key="order-1024",
)
# REQUIRES_ACTION: send the customer to the provider's page.
redirect_to = payment["next_action"]["url"]The customer pays on the provider's page and comes back to your return_url. The return is not the outcome: the outcome is the webhook (payment.authorized, payment.captured, payment.declined, ...). Amounts are integers in the currency's minor unit; how the scale changes per currency.
Capture, void, refund
from payshen import PayshenError
payshen.payments.capture(payment_id, idempotency_key="capture-order-1024") # in full
payshen.payments.capture(payment_id, amount=3000, idempotency_key="capture-order-1024") # or in part
payshen.payments.void(payment_id, idempotency_key="void-order-1024") # before capture
try:
payshen.payments.refund(payment_id, amount=2000, idempotency_key="refund-order-1024-1")
except PayshenError as e:
# 409 refund_pending: accepted, not failed. The provider confirms a moment later and
# payment.refunded or payment.refund_failed follows. Do not send it again under a new key.
if e.code != "refund_pending":
raiseA capture or void a provider only accepted answers with the payment PENDING; it becomes CAPTURED or CANCELLED when the provider confirms, and the webhook follows.
Reading payments and events
payment = payshen.payments.get(payment_id)
timeline = payshen.payments.events(payment_id)
page = payshen.payments.list(limit=50, status="captured")
for payment in payshen.payments.all(status="captured"):
... # every page, newest first
for event in payshen.events.all(type="payment.captured"):
... # the same events webhooks deliver, for pollinglist returns one page; all is an iterator that follows the cursor for you.
Idempotency
Every POST carries an Idempotency-Key. Pass your own as idempotency_key (an order or refund reference is a good one); without one the client makes one and reuses it across its own retries. The same key with a different body is refused with 422, so a retry can never charge twice.
Verifying webhooks
Verify the signature against the raw body, then act on the event.
An empty secret never verifies anything: missing_secret is raised instead, so an unset environment variable fails loudly rather than letting a forged event through (0.1.2).
import os
from flask import Flask, request
from payshen import SignatureVerificationError, parse_webhook_event
app = Flask(__name__)
@app.post("/payshen/webhooks")
def payshen_webhook():
raw = request.get_data() # the RAW body: one re-encoded from json never verifies
try:
event = parse_webhook_event(
raw, request.headers.get("X-Payshen-Signature"), os.environ["PAYSHEN_WEBHOOK_SECRET"]
)
except SignatureVerificationError:
return "", 400
if event["type"] == "payment.captured":
fulfil(event["data"]["reference"])
return "", 204The signature proves a delivery came from Payshen and is recent (5 minutes by default); it does not make it new. Deliveries are retried, so the same event can arrive twice. claim_webhook_event verifies, parses and takes each event once, with a store you back with your database:
from payshen import claim_webhook_event
event = claim_webhook_event(raw, header, secret, store)
if event is None:
return "", 204 # already handledErrors
AuthenticationError: 401 or 403: a missing, wrong or revoked key, or one not allowed this call.ValidationError: 422: refused as sent. code names the rule.RateLimitError: 429 after the client's own retries. retry_after_seconds when the API said.NetworkError: No answer at all. On a POST, retry with the same idempotency key.PayshenError: Everything else, and the parent of all of the above.
Every exception has status, code (refund_pending, not_capturable, payment_result_unknown, ...) and request_id. Branch on code, never on the message, and log request_id so support can find the call. Every code is in the reference.
Retries and timeouts
The client retries 408, 425, 429, 500, 502, 503 and 504 answers with backoff and jitter, up to max_retries (3), honouring Retry-After. It never retries an answer the API says a retry cannot improve, such as payment_result_unknown: there the payment may have happened, so read it before doing anything else. timeout is per attempt, 20 seconds by default.
Rotating a webhook secret
During a rotation pass both secrets, and drop the old one once every delivery is signed with the new:
event = parse_webhook_event(raw, header, [new_secret, old_secret])Everything else
payshen.insights, payshen.suggestions and, for white-label partners, payshen.merchants cover the rest of the API. payshen.request("GET", "/api/v1/...") calls any endpoint and returns the whole answer.
Before going live
The checklist is the same in every language: the go-live list in the TypeScript guide.