Platico
Prijava

Python SDK

platico (Python SDK)

Zvanični Python SDK za Platico API. Stripe-oblikovan, nula runtime zavisnosti, Python 3.9+. Ovaj vodič vas vodi od nule do uspešne naplate za 5 minuta.

1. Instalacija i ključevi

pip install platico

Sa stranice API ključevi uzmite dva ključa (oba se kreiraju automatski u test režimu):

  • Publishable ključ (pk_test_…) — ide u vaš frontend. Tajan nije; može samo da tokenizuje karticu, ne i da naplati.
  • Secret ključ (sk_test_…) — ide na vaš server. Nikada ga ne izlažite u browseru.

2. Frontend — naplata kartice sa platico.js

platico.js ubacuje polja za broj kartice i CVV kao bezbedne iframe-ove i vraća jednokratni token. Podaci kartice nikada ne dodiruju vaš server (ostajete van PCI DSS SAQ D). Frontend je isti bez obzira na jezik servera.

<!-- 1. Učitaj platico.js -->
<script src="https://js.platico.rs/v1/platico.js"></script>

<!-- 2. Mesta gde se montiraju iframe-ovi -->
<div id="card-number"></div>
<div id="card-cvv"></div>
<button id="pay">Plati 49,90 RSD</button>
const platico = Platico('pk_test_...', { apiBase: 'https://api.platico.rs' })

await platico.mountCard({ cardNumber: '#card-number', cvv: '#card-cvv' })

document.getElementById('pay').addEventListener('click', async () => {
  // Tokenizuj karticu — vraća jednokratni payment token.
  const { token } = await platico.createPaymentToken({
    cardholder: 'Petar Petrović',
    expMonth: '12',
    expYear: '2030',
  })

  // Pošalji token SVOM serveru (nikada secret ključ u browser).
  const res = await fetch('/checkout', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token }),
  })
  const intent = await res.json()

  // 3-D Secure: ako je potrebna akcija, otvori izazov.
  if (intent.status === 'requires_action') {
    await platico.handleNextAction(intent)
    // Po povratku ponovo proveri status preko svog servera.
  }
})

3. Server — kreiraj i potvrdi naplatu

import os
from flask import Flask, request, jsonify
from platico import Platico

client = Platico(os.environ["PLATICO_API_KEY"])  # sk_test_...
app = Flask(__name__)

@app.post("/checkout")
def checkout():
    data = request.get_json()
    intent = client.payment_intents.create({
        "amount": 4990,                 # minor units — 49,90 RSD
        "currency": "rsd",
        "confirm": True,                # odmah potvrdi
        "payment_method_token": data["token"],
        # Stranica NA VAŠEM sajtu na koju se kupac vraća posle 3DS-a.
        "return_url": "https://vas-sajt.rs/checkout/complete",
    })
    # status: "succeeded" | "requires_action" | "requires_payment_method"
    return jsonify(intent)

4. 3-D Secure

Ako kartica zahteva 3DS, status je requires_action, a intent["next_action"]["redirect_to_url"]["url"] nosi izazov. Frontend ga otvara sa platico.handleNextAction(intent) (popup). Postavite return_url (u koraku 3) na stranicu NA VAŠEM sajtu — handleNextAction se razrešava čim se popup vrati na nju. Zatim ponovo dohvatite PaymentIntent na serveru.

5. Dohvati konačni status

updated = client.payment_intents.retrieve(intent["id"])

if updated["status"] == "succeeded":
    # Naplata uspela — ispuni narudžbinu.
    ...

Pri dohvatanju, Platico lenjo usaglašava status sa procesorom — tako PI prelazi u succeeded i bez javnog webhook tunela na localhost-u.

6. Sandbox i test kartice

Test režim koristi sk_test_ / pk_test_ i AllSecure sandbox procesor. Bazni URL API-ja je https://api.platico.rs.

KarticaBrojRezultat
Visa4111 1111 1111 1111uspeh
Visa4242 4242 4242 4242neuspeh
Mastercard5555 5555 5555 4444uspeh
Maestro5000 0010 0000 0007uspeh

Bilo koji datum u budućnosti, bilo koji 3-cifreni CVV, bilo koje ime.

Očekivano: kartica 4111 ima obavezan 3DS, pa prvi odgovor uvek bude requires_action — to nije greška. Na sandbox 3DS ekranu izaberite Authenticated (ECI 05) da simulirate uspešan izazov; zatim ponovo dohvatite PI i videćete succeeded.

Sačuvaj karticu i naplati ponovo (povratni kupac)

# 1) jednom — napravi kupca i sačuvaj tokenizovanu karticu na njemu
customer = client.customers.create({
    "email": "jovan@example.rs",
    "name": "Jovan Petrović",
})
pm = client.payment_methods.create({
    "type": "card",
    "card": {"token": token},       # token iz platico.js createPaymentToken()
    "customer": customer["id"],
})

# 2) kasnije — naplati sačuvanu karticu; bez platico.js, bez unosa kartice
client.payment_intents.create({
    "amount": 1990,
    "currency": "rsd",
    "customer": customer["id"],
    "payment_method": pm["id"],
    "confirm": True,
})
# pm["card"] → {brand, last4, exp_month, exp_year} za prikaz

Autorizuj sada, naplati kasnije

Rezerviši iznos pri narudžbini i naplati kad pošalješ — prosledi capture_method="manual".

pi = client.payment_intents.create({
    "amount": 4990,
    "currency": "rsd",
    "capture_method": "manual",
    "confirm": True,
    "payment_method_token": token,
})                                    # status: "requires_capture"

client.payment_intents.capture(pi["id"])   # naplati sredstva
client.payment_intents.cancel(pi["id"])    # …ili otpusti rezervaciju

Povraćaji

client.refunds.create({"payment_intent": pi["id"]})          # pun povraćaj

client.refunds.create({                                       # delimičan (10,00 RSD)
    "payment_intent": pi["id"],
    "amount": 1000,
    "reason": "requested_by_customer",
})

Liste i auto-paginacija

# prođi kroz sve rezultate — jedna stranica u memoriji
for pi in client.payment_intents.list({"status": "succeeded"}):
    print(pi["id"], pi["amount"])

# ili sakupi ograničen broj (limit je obavezan — sprečava OOM)
recent = client.customers.list().auto_paging_to_list(limit=100)

Rukovanje greškama

from platico import CardError, RateLimitError, ApiError

try:
    client.payment_intents.create({ ... })
except CardError as err:
    # err.code / err.decline_code — npr. "card_declined" / "insufficient_funds"
    ...
except RateLimitError as err:
    # pokušaj ponovo posle err.retry_after sekundi
    ...
except ApiError as err:
    # err.status_code, err.request_id — navedi request_id u tiketu podrške
    ...

Prosledi idempotency ključ na svakom mutirajućem pozivu da retry bude bezbedan:

client.payment_intents.create(params, idempotency_key=f"order_{order_id}")

Sporovi (chargeback-ovi)

for d in client.disputes.list({"status": "needs_response"}):
    print(d["id"], d["amount"], d["reason"])

dispute = client.disputes.retrieve("dp_...")

Događaji (events)

Isti događaji koji pokreću webhook-ove, dostupni iz koda. Ograničeni na režim vašeg ključa.

# auto-paginacija; filtriraj po tipu
for ev in client.events.list({"type": "payment_intent.succeeded"}):
    print(ev["id"], ev["type"], ev["data"]["object"])

event = client.events.retrieve("evt_...")

Webhook endpoint-ovi

Upravljaj odredištima za isporuku iz koda. Potpisni secret se vraća JEDNOM — pri kreiranju i roll-u — pa ga tada sačuvaj; koristi ga za verifikaciju isporuka.

endpoint = client.webhook_endpoints.create({
    "url": "https://vas-sajt.rs/webhooks/platico",
    "enabled_events": ["payment_intent.succeeded", "charge.refunded"],
})
print(endpoint["secret"])  # whsec_… — prikazano jednom

client.webhook_endpoints.list()
client.webhook_endpoints.update(endpoint["id"], {"status": "disabled"})
client.webhook_endpoints.roll(endpoint["id"])    # rotiraj potpisni ključ
client.webhook_endpoints.delete(endpoint["id"])

Režim endpoint-a prati ključ (sk_test_ → test, sk_live_ → live).

Webhook verifikacija

import os
from flask import Flask, request
from platico import (
    construct_event,
    WebhookSignatureInvalidError,
    WebhookTimestampStaleError,
    WebhookMalformedHeaderError,
)

app = Flask(__name__)

# VAŽNO: webhook-ovi treba SIROVI body (bajtovi). request.get_json()
# re-serijalizuje JSON i HMAC više ne odgovara — koristi request.get_data().
@app.post("/webhooks/platico")
def webhook():
    try:
        event = construct_event(
            request.get_data(),                            # sirovi bajtovi
            request.headers.get("Platico-Signature", ""),
            os.environ["PLATICO_WEBHOOK_SECRET"],
        )
    except (
        WebhookSignatureInvalidError,
        WebhookTimestampStaleError,
        WebhookMalformedHeaderError,
    ):
        return "", 400

    print(event["type"], event["data"]["object"])
    return "", 200

Konfiguracija

Platico(
    api_key,
    api_base="https://api.platico.rs",   # podrazumevano
    timeout=30.0,                          # sekunde, podrazumevano
    max_network_retries=3,                 # 5xx / 429 / mreža
    telemetry=False,                       # opt-in
)

Content-Security-Policy

Ako vaša checkout stranica koristi CSP, dozvolite ove hostove:

script-src  https://js.platico.rs https://asxgw.paymentsandbox.cloud https://asxgw.com
frame-src   https://asxgw.paymentsandbox.cloud https://asxgw.com https://secure.asxgw.com
connect-src https://api.platico.rs https://asxgw.paymentsandbox.cloud https://asxgw.com
img-src     https://asxgw.paymentsandbox.cloud https://asxgw.com

Sigurnost

  • SDK uvek verifikuje TLS (lanac sertifikata + hostname) — bez opcije za isključivanje.
  • API ključ se NIKADA ne pojavljuje u serijalizovanim greškama (ni u traceback-u/pickle-u).
  • HMAC verifikacija webhook-a koristi hmac.compare_digest (konstantno vreme) — nikada ==.
  • Auto Idempotency-Key na svakom mutirajućem pozivu; isti ključ na svim retry-jevima.
  • Retry samo na 5xx / 429 / network greške. Nikada na ostale 4xx.
  • Nula runtime zavisnosti — samo standardna biblioteka.

Provera onoga što ste instalirali

pip install platico

Od prvog CI izdanja, svaka verzija se objavljuje preko PyPI Trusted Publishing-a (OIDC) i nosi PEP 740 atestaciju koja povezuje artefakt sa konkretnim GitHub Actions workflow run-om.

Linkovi