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 sessions
  • checkout:read — Read checkout sessions
  • payments:read — Read payments and transactions
  • payments:refund — Create refunds
  • products:read — Read products
  • products:write — Create, update and delete products
  • customers:read — Read customers
  • payouts:read — Read payouts
  • webhooks:read — Read webhook endpoints, events and deliveries
  • webhooks:write — Manage webhook endpoints and replay events
  • orders:write — Approve, reject and cancel orders
  • test:simulate — Run test-mode simulations
  • developer:read — Read request logs and diagnostics

Conventions

  • Money: integers in minor units plus currency (ISO 4217) and currency_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) and cursor; responses are { data, has_more, next_cursor }.
  • Request ids: every response has X-Request-Id; include it in support requests. Send your own X-Correlation-Id to tie records together.
  • Rate limits: 600 reads / 240 writes per minute per key; see X-RateLimit-* and Retry-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 key
  • 403 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 account
  • 409 — invalid_state (e.g. refunding a failed payment), conflict, idempotency_conflict
  • 422 validation_error — details lists every failing field
  • 429 rate_limit_error — honour Retry-After
  • 502/503 provider_error — the payment provider failed or is unavailable; details.retryable says whether to retry

Endpoints

Machine-readable version: /api/openapi.json. Try any of these from the playground.

Account

GET/api/v1/accountAuthenticated seller account

Providers

GET/api/v1/providersProvider capability registry
GET/api/v1/providers/{id}One provider
POST/api/v1/providers/{id}/connectStart provider onboardingproducts:write
GET/api/v1/connected-accountsConnected provider accounts

Products

GET/api/v1/productsList productsproducts:read
POST/api/v1/productsCreate 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/sessionsCreate checkout sessioncheckout:writeidempotent
GET/api/v1/checkout/sessionsList checkout sessionscheckout:read
GET/api/v1/checkout/sessions/{id}Retrieve checkout sessioncheckout:read

Payments

GET/api/v1/paymentsList paymentspayments:read
GET/api/v1/payments/{id}Retrieve paymentpayments:read
POST/api/v1/payments/{id}/refundsRefund paymentpayments:refundidempotent
GET/api/v1/payments/{id}/refundsList refunds for paymentpayments:read
GET/api/v1/refundsList refundspayments:read
GET/api/v1/transactionsLedger entries (seller account)payments:read
GET/api/v1/payoutsList payoutspayouts:read

Customers

GET/api/v1/customersList customerscustomers:read

Orders

GET/api/v1/ordersList orderspayments:read
GET/api/v1/orders/{id}Retrieve order with transition timelinepayments:read
POST/api/v1/orders/{id}/approveApprove orderorders:writeidempotent
POST/api/v1/orders/{id}/rejectReject orderorders:writeidempotent
POST/api/v1/orders/{id}/cancelCancel orderorders:writeidempotent
POST/api/v1/orders/{id}/expireExpire orderorders:writeidempotent

Webhooks

GET/api/v1/webhook-endpointsList webhook endpointswebhooks:read
POST/api/v1/webhook-endpointsCreate 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}/testSend a test event (test mode)webhooks:writetest only
POST/api/v1/webhook-endpoints/{id}/verifyMark verified after a successful deliverywebhooks:write
POST/api/v1/webhook-endpoints/{id}/rotate-secretRotate signing secretwebhooks:write
GET/api/v1/webhook-eventsList eventswebhooks:read
GET/api/v1/webhook-events/{id}Retrieve eventwebhooks:read
GET/api/v1/webhook-events/{id}/deliveriesDeliveries for an eventwebhooks:read
GET/api/v1/webhook-deliveries/{id}Retrieve delivery attemptwebhooks:read
POST/api/v1/webhook-deliveries/{id}/replayReplay delivery (new attempt, same event)webhooks:write

Test lab

POST/api/v1/test/checkout-sessionsCreate a TEST checkout + payment (no provider)test:simulatetest onlyidempotent
GET/api/v1/test/transactionsList 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}/simulateSimulate a scenario on a test paymenttest:simulatetest only
POST/api/v1/test/orders/{id}/approveApprove test orderorders:writetest onlyidempotent
POST/api/v1/test/orders/{id}/rejectReject test orderorders:writetest onlyidempotent
POST/api/v1/test/orders/{id}/cancelCancel test orderorders:writetest onlyidempotent
POST/api/v1/test/orders/{id}/expireExpire test orderorders:writetest onlyidempotent

Developer

GET/api/v1/developer/requestsAPI request logdeveloper:read
GET/api/v1/developer/requests/{id}Request log entry with redacted bodiesdeveloper:read
POST/api/v1/developer/diagnosticsRun integration diagnostics against an endpointwebhooks:writetest only
GET/api/v1/developer/diagnosticsList 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 id and 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.