Yoco — payment gateway integration profile
Also known as: Yoco Online
Yoco Checkout
Yoco Payment Gateway
Category: sa-gateway
Regions: South Africa
Countries: ZA
Currencies: ZAR
Payment methods: card

Authentication:
model: bearer-secret-key
key_types: sk_test_
sk_live_
pk_test_
pk_live_
whsec_
sandbox_instant: yes
kyc_required_for_live: yes


Payment initiation:
flow: redirect
endpoint: POST https://payments.yoco.com/api/checkouts
http_method: POST
required_params: amount
currency
returns: id
redirectUrl
status
amount_unit: minor-subunit


Status handling:
mechanisms: webhook
poll
redirect-return
webhook_verification: Svix scheme. Headers 'webhook-id', 'webhook-timestamp', 'webhook-signature'. Strip 'whsec_' prefix, base64-decode the secret, HMAC-SHA256 over '{webhook-id}.{webhook-timestamp}.{rawBody}', base64-compare; signature header is space-separated 'v1,<b64sig>' entries; reject outside a 3-minute replay window. RAW body required.
raw_status_values: created
started
processing
completed
failed
cancelled
payment.succeeded
payment.failed
refund.succeeded
status_map: created: pending
started: pending
processing: pending
completed: paid
payment.succeeded: paid
failed: failed
cancelled: failed
payment.failed: failed
refunded: refunded
refund.succeeded: refunded

Capabilities:
redirect: yes
poll: yes
webhook: yes
mobile_push: no
refund: yes
recurring: no

Credentials required:
key: secretKey
label: Secret Key (sk_)
secret: yes
note: server-side auth for all Checkout API calls; sk_test_ vs sk_live_ selects the mode (single host)
key: webhookSecret
label: Webhook Signing Secret (whsec_)
secret: yes
note: returned ONCE when the webhook is registered via POST /api/webhooks; ManishaPay captures and stores it — the merchant does not supply it

Sandbox:
test_credentials: note: test keys are instant from the Yoco dashboard (Sell Online -> Payment Gateway -> API keys), no KYC; live keys require KYC
test_cards: success
decline
notes: Any future expiry, any CVV. ZAR only, card only, minimum charge R2.00 (200 cents).

Known pain points and mitigations:
problem: 100x over/undercharge
cause: amount sent in Rand instead of cents (Yoco expects minor subunits)
mitigation: Single major-unit amount in the ManishaPay API; convert to integer cents internally (× 100) and guard the R2.00 minimum before the call
severity: high
problem: Charge rejected as below minimum
cause: amount below R2.00 (200 cents), Yoco's hard floor
mitigation: Enforce a 200-cent minimum locally and return a clear resolution before hitting the API
severity: medium
problem: Order marked paid on a spoofed / premature successUrl redirect
cause: treating the browser return to successUrl as proof of payment
mitigation: Never trust successUrl; confirm on the payment.succeeded webhook or re-fetch GET /checkouts/{id} server-side before fulfilling
severity: critical
problem: Webhook signature verification fails
cause: Svix scheme is easy to get wrong — signing the wrong content, not base64-decoding the whsec_ secret, or hashing a re-serialised body
mitigation: One central Svix verifier: strip whsec_, base64-decode the secret, HMAC-SHA256 over '{id}.{timestamp}.{rawBody}' on the RAW bytes, base64 constant-time compare, 3-minute replay window
severity: critical
problem: Double-charge on retry
cause: no idempotency applied by default; a network retry re-creates the checkout
mitigation: Attach an Idempotency-Key header on every create/refund (derived from the merchant reference)
severity: high
problem: Sparse / shifting documentation
cause: Yoco's online gateway docs are thin and occasionally change endpoints
mitigation: Hide Yoco behind the ManishaPay neutral API so integrators never touch the raw surface
severity: medium
problem: No native recurring or disputes API
cause: Yoco Online is single-charge, card-only; disputes are not exposed programmatically
mitigation: Emulate recurring in ManishaPay (stored consent + scheduled charges) or route recurring elsewhere; disputes cannot be auto-mapped, so 'disputed' is never emitted
severity: medium

SDKs:
node: none — REST-direct
php: yoco/yoco-php (light, unofficial)
rest_direct_ok: yes

Notes: South Africa only, ZAR only, card only (all 3DS-backed), minimum charge R2.00. No EFT, no recurring, no disputes API. Single host payments.yoco.com — TEST vs LIVE is decided by the sk_test_/sk_live_ key prefix. Webhook secret (whsec_) is issued once on POST /api/webhooks registration and captured by ManishaPay, not supplied by the merchant. Refunds via POST /api/checkouts/{id}/refund (optional partial amount in cents).

Fields not yet cross-checked against official docs: exact checkout raw_status_values enum
public-key (pk_) usage in server flow