API 2026-01-01
SkyPay API
A versioned REST API under /api/v1. JSON in, JSON out. Every endpoint below exists and is backed by real server-side functionality; nothing is listed that is not implemented.
Authentication
Create keys in Developer lab → API keys. Send them as a bearer token. Keys are either test (sp_test_…) or live (sp_live_…) and carry explicit scopes; the seller identity is always derived from the key, never from the request body.
curl https://skypay.to/api/v1/account \
-H "Authorization: Bearer sp_test_…"Scopes
checkout:write— Create checkout sessionscheckout:read— Read checkout sessionspayments:read— Read payments and transactionspayments:refund— Create refundsproducts:read— Read productsproducts:write— Create, update and delete productscustomers:read— Read customerspayouts:read— Read payoutswebhooks:read— Read webhook endpoints, events and deliverieswebhooks:write— Manage webhook endpoints and replay eventsorders:write— Approve, reject and cancel orderstest:simulate— Run test-mode simulationsdeveloper:read— Read request logs and diagnostics
Conventions
- Money: integers in minor units plus
currency(ISO 4217) andcurrency_exponent. JPY has exponent 0, KWD 3. Never send decimals. - Ids: prefixed (
prod_,chk_,pay_,evt_,whe_,whd_…). Test-mode ids carry a_test_infix. - Pagination:
limit(max 100) andcursor; responses are{ data, has_more, next_cursor }. - Request ids: every response has
X-Request-Id; include it in support requests. Send your ownX-Correlation-Idto tie records together. - Rate limits: 600 reads / 240 writes per minute per key; see
X-RateLimit-*andRetry-After. - Modes: a test key only sees test resources; live keys only live resources. Test-only routes (
/api/v1/test/*) reject live keys outright.
Idempotency
Payment-creating POSTs (/checkout/sessions, /payments/{id}/refunds, order actions, test checkouts) accept an Idempotency-Key header (1–255 chars). Retrying with the same key and body returns the original response with Idempotent-Replayed: true; the same key with a different body is rejected with idempotency_conflict. Keys expire after 24 hours and are scoped to your account and mode.
Errors
{
"error": {
"type": "invalid_request_error",
"code": "validation_error",
"message": "The request could not be validated.",
"details": [
{
"path": "product_id",
"message": "Invalid input: expected string, received undefined",
"code": "invalid_type"
}
],
"request_id": "req_…"
}
}401 authentication_error— missing or invalid key403 permission_error— missing scope, suspended account, or live key on a test-only route (live_mode_forbidden)404— not found, including resources owned by another account409—invalid_state(e.g. refunding a failed payment),conflict,idempotency_conflict422 validation_error—detailslists every failing field429 rate_limit_error— honourRetry-After502/503 provider_error— the payment provider failed or is unavailable;details.retryablesays whether to retry
Endpoints
Machine-readable version: /api/openapi.json. Try any of these from the playground.
Account
| GET | /api/v1/account | Authenticated seller account |
Providers
| GET | /api/v1/providers | Provider capability registry |
| GET | /api/v1/providers/{id} | One provider |
| POST | /api/v1/providers/{id}/connect | Start provider onboardingproducts:write |
| GET | /api/v1/connected-accounts | Connected provider accounts |
Products
| GET | /api/v1/products | List productsproducts:read |
| POST | /api/v1/products | Create productproducts:write |
| GET | /api/v1/products/{id} | Retrieve productproducts:read |
| PATCH | /api/v1/products/{id} | Update productproducts:write |
| DELETE | /api/v1/products/{id} | Delete productproducts:write |
Checkout
| POST | /api/v1/checkout/sessions | Create checkout sessioncheckout:writeidempotent |
| GET | /api/v1/checkout/sessions | List checkout sessionscheckout:read |
| GET | /api/v1/checkout/sessions/{id} | Retrieve checkout sessioncheckout:read |
Payments
| GET | /api/v1/payments | List paymentspayments:read |
| GET | /api/v1/payments/{id} | Retrieve paymentpayments:read |
| POST | /api/v1/payments/{id}/refunds | Refund paymentpayments:refundidempotent |
| GET | /api/v1/payments/{id}/refunds | List refunds for paymentpayments:read |
| GET | /api/v1/refunds | List refundspayments:read |
| GET | /api/v1/transactions | Ledger entries (seller account)payments:read |
| GET | /api/v1/payouts | List payoutspayouts:read |
Customers
| GET | /api/v1/customers | List customerscustomers:read |
Orders
| GET | /api/v1/orders | List orderspayments:read |
| GET | /api/v1/orders/{id} | Retrieve order with transition timelinepayments:read |
| POST | /api/v1/orders/{id}/approve | Approve orderorders:writeidempotent |
| POST | /api/v1/orders/{id}/reject | Reject orderorders:writeidempotent |
| POST | /api/v1/orders/{id}/cancel | Cancel orderorders:writeidempotent |
| POST | /api/v1/orders/{id}/expire | Expire orderorders:writeidempotent |
Webhooks
| GET | /api/v1/webhook-endpoints | List webhook endpointswebhooks:read |
| POST | /api/v1/webhook-endpoints | Create webhook endpoint (secret returned once)webhooks:write |
| GET | /api/v1/webhook-endpoints/{id} | Retrieve endpointwebhooks:read |
| PATCH | /api/v1/webhook-endpoints/{id} | Update endpointwebhooks:write |
| DELETE | /api/v1/webhook-endpoints/{id} | Delete endpointwebhooks:write |
| POST | /api/v1/webhook-endpoints/{id}/test | Send a test event (test mode)webhooks:writetest only |
| POST | /api/v1/webhook-endpoints/{id}/verify | Mark verified after a successful deliverywebhooks:write |
| POST | /api/v1/webhook-endpoints/{id}/rotate-secret | Rotate signing secretwebhooks:write |
| GET | /api/v1/webhook-events | List eventswebhooks:read |
| GET | /api/v1/webhook-events/{id} | Retrieve eventwebhooks:read |
| GET | /api/v1/webhook-events/{id}/deliveries | Deliveries for an eventwebhooks:read |
| GET | /api/v1/webhook-deliveries/{id} | Retrieve delivery attemptwebhooks:read |
| POST | /api/v1/webhook-deliveries/{id}/replay | Replay delivery (new attempt, same event)webhooks:write |
Test lab
| POST | /api/v1/test/checkout-sessions | Create a TEST checkout + payment (no provider)test:simulatetest onlyidempotent |
| GET | /api/v1/test/transactions | List test paymentspayments:readtest only |
| GET | /api/v1/test/transactions/{id} | Test payment with ledger, events and timelinepayments:readtest only |
| POST | /api/v1/test/transactions/{id}/simulate | Simulate a scenario on a test paymenttest:simulatetest only |
| POST | /api/v1/test/orders/{id}/approve | Approve test orderorders:writetest onlyidempotent |
| POST | /api/v1/test/orders/{id}/reject | Reject test orderorders:writetest onlyidempotent |
| POST | /api/v1/test/orders/{id}/cancel | Cancel test orderorders:writetest onlyidempotent |
| POST | /api/v1/test/orders/{id}/expire | Expire test orderorders:writetest onlyidempotent |
Developer
| GET | /api/v1/developer/requests | API request logdeveloper:read |
| GET | /api/v1/developer/requests/{id} | Request log entry with redacted bodiesdeveloper:read |
| POST | /api/v1/developer/diagnostics | Run integration diagnostics against an endpointwebhooks:writetest only |
| GET | /api/v1/developer/diagnostics | List diagnosticsdeveloper:read |
| GET | /api/v1/developer/diagnostics/{id} | Retrieve diagnosticdeveloper:read |
Example: create a checkout session
POST https://skypay.to/api/v1/checkout/sessions
Authorization: Bearer sp_test_…
Idempotency-Key: order-1042
Content-Type: application/json
{ "product_id": "prod_test_…", "success_url": "https://example.com/thanks", "cancel_url": "https://example.com/cancel", "customer_email": "[email protected]" }Redirect the buyer to the returned url. Payment confirmation arrives on your webhook as payment.succeeded; do not trust the redirect to success_url as proof of payment.
Webhooks
Register endpoints in the dashboard or via POST /api/v1/webhook-endpoints. Each endpoint gets a signing secret (whs_…) shown once. Every delivery is an HTTP POST with the event envelope and these headers:
SkyPay-Signature:t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>SkyPay-Event-Id,SkyPay-Delivery-Id,SkyPay-Livemode,SkyPay-Version
Envelope
{
"id": "evt_test_example",
"object": "event",
"type": "payment.succeeded",
"api_version": "2026-01-01",
"created_at": "2026-01-01T12:00:00Z",
"livemode": false,
"resource": {
"type": "payment",
"id": "pay_test_example"
},
"data": {
"id": "pay_test_example",
"object": "payment",
"status": "succeeded",
"amount": 10000,
"currency": "AUD",
"currency_exponent": 2,
"checkout_session_id": "chk_test_example"
},
"correlation_id": "cor_…"
}Verifying (Node.js)
import crypto from "node:crypto";
export function verifySkyPaySignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false; // replay protection
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return expected.length === parts.v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
// Express: app.post("/webhooks/skypay", express.raw({ type: "application/json" }), (req, res) => {
// if (!verifySkyPaySignature(req.body.toString(), req.get("SkyPay-Signature"), process.env.SKYPAY_WEBHOOK_SECRET)) return res.status(400).end();
// const event = JSON.parse(req.body);
// if (alreadyProcessed(event.id)) return res.status(200).end(); // idempotent on event id
// handle(event); res.status(200).end();
// });Verifying (Python)
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Delivery policy
- Respond 2xx within 10 seconds. Anything else (including a timeout) counts as a failure.
- Transient failures (network, 5xx, 408, 429) are retried with exponential backoff and jitter, up to 8 attempts over roughly 2 hours, then dead-lettered. Other 4xx responses are not retried.
- Events may arrive more than once and out of order. Make handlers idempotent on
idand compare timestamps. - Secrets can be rotated; the previous secret stays valid for 24 hours.
- Replaying from the dashboard re-sends the original payload with a new signature timestamp.
Event types
checkout.session.created · checkout.session.completed · checkout.session.expired · payment.created · payment.processing · payment.succeeded · payment.failed · payment.cancelled · payment.refunded · payment.disputed · refund.created · refund.succeeded · refund.failed · dispute.created · dispute.updated · dispute.closed · payout.pending · payout.in_transit · payout.paid · payout.failed · seller.account.updated · order.created · order.pending_approval · order.approved · order.rejected · order.cancelled · order.expired · order.fulfilled · subscription.created · subscription.updated · subscription.cancelled · test.ping
checkout.js (embeddable checkout)
<script src="https://skypay.to/checkout.js" defer></script>
<button data-skypay-product="prod_…" data-skypay-quantity="1" data-skypay-success-url="https://example.com/thanks">Buy now</button>
<script>
// Programmatic use
SkyPay.checkout({ product: "prod_…", email: "[email protected]", reference: "order-1042" });
// Create without redirecting
SkyPay.createSession({ product: "prod_…" }).then((s) => console.log(s.url));
SkyPay.onError = (err, button) => console.warn(err.message);
</script>The script only needs a public product id; the session is created server-side with the product's price, currency and the seller's fee rule. It never accepts a price, seller id or connected account from the page. Integrations written against the former window.FluxPay global and data-fluxpay-* attributes keep working (deprecated).
SDKs
A packaged SDK is not published yet; the API is plain JSON over HTTPS and the examples above are complete. The following snippets are tested against the live routes.
Node.js / TypeScript
const res = await fetch("https://skypay.to/api/v1/checkout/sessions", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.SKYPAY_KEY}`, "Content-Type": "application/json", "Idempotency-Key": orderId },
body: JSON.stringify({ product_id: "prod_…", success_url: "https://example.com/thanks", customer_email: email }),
});
const session = await res.json();
if (!res.ok) throw new Error(session.error.message);
redirect(session.url);Python
import requests
r = requests.post("https://skypay.to/api/v1/checkout/sessions",
headers={"Authorization": f"Bearer {KEY}", "Idempotency-Key": order_id},
json={"product_id": "prod_…", "success_url": "https://example.com/thanks"})
r.raise_for_status(); print(r.json()["url"])PHP
$ch = curl_init("https://skypay.to/api/v1/checkout/sessions");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key", "Content-Type: application/json", "Idempotency-Key: $orderId"],
CURLOPT_POSTFIELDS => json_encode(["product_id" => "prod_…", "success_url" => "https://example.com/thanks"])]);
$session = json_decode(curl_exec($ch), true); header("Location: " . $session["url"]);Go
body, _ := json.Marshal(map[string]string{"product_id": "prod_…", "success_url": "https://example.com/thanks"})
req, _ := http.NewRequest("POST", "https://skypay.to/api/v1/checkout/sessions", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+key); req.Header.Set("Content-Type", "application/json"); req.Header.Set("Idempotency-Key", orderID)
resp, err := http.DefaultClient.Do(req)Developer lab
The dashboard includes a playground (real requests with a short-lived grant for a test key, so secrets never reach the browser), a request log, test transactions with simulations that drive the same state machine as production, manual order approval, the webhook delivery inspector, replay, and "Test my integration" diagnostics. Simulation routes require a test key and reject live resource ids by design.