OrcaPayz

API reference

OrcaPayz API v1

One REST API for every Stripe account you connect. Create payment intents, let the router pick the account, and get signed webhooks back.

Base URL http://orcapayz.com/api/v1 JSON in / JSON out 120 requests / minute Version 1.0.0

Introduction

OrcaPayz is an orchestration layer in front of your own Stripe accounts. You integrate one API; we create the PaymentIntent on the account that your routing strategy and per-account limits select, serve a white-label checkout on your payment domain, and notify you with signed webhooks. Your Stripe accounts remain the merchant of record and hold the funds.

All endpoints live under http://orcapayz.com/api/v1. Requests and responses are JSON. Amounts are decimal major units (for example 49.90, not 4990). Every response carries a boolean success field.

Authentication

Create a key pair in Dashboard → API keys and send both values as headers on every request to /api/v1.

X-API-Key: ok_live_…
X-API-Secret: os_live_…
Content-Type: application/json
Accept: application/json

Keys are scoped to your merchant account and can be rotated from the dashboard at any time. Keep the secret on your server; never ship it to a browser or mobile app. Requests are limited to 120 per minute per key; over the limit you receive HTTP 429 with a Retry-After header.

Errors

Errors return a non-2xx status and a JSON body with success: false, a human-readable message, and a machine-readable code where applicable. Validation failures (422) add an errors object keyed by field.

HTTP/1.1 422 Unprocessable Content
{
  "success": false,
  "message": "The given data was invalid.",
  "errors": {
    "amount": [ "The amount must be at least 0.50." ],
    "return_url": [ "The return url field is required." ]
  }
}
StatusCodeMeaning
401 MISSING_CREDENTIALS One or both auth headers are missing.
401 INVALID_CREDENTIALS The key/secret pair does not match an active key.
403 ACCESS_EXPIRED Your trial or subscription has ended. Contact us to continue.
403 INVALID_PAYMENT_ACCOUNT payment_account_id is not one of your active accounts.
400 NO_AVAILABLE_ACCOUNT No active, verified account can take this payment right now.
400 ACCOUNT_LIMIT_EXCEEDED The selected account would exceed its daily or monthly limit.
400 CANNOT_CANCEL The transaction is not pending or processing.
400 REFUND_FAILED Stripe rejected the refund or the amount exceeds the refundable balance.
422 validation Request body failed validation; see errors.
429 rate limit More than 120 requests in one minute.
502 PROVIDER_ERROR Stripe returned an error while creating or updating the intent.

Health check

GET http://orcapayz.com/api/health — no authentication.

Use it for uptime monitors and connectivity checks from your infrastructure.

{
  "success": true,
  "status": "ok",
  "version": "1.0.0",
  "timestamp": "2026-09-27T10:15:32+00:00"
}

Create a payment intent

POST /api/v1/payment-intents

Creates a transaction, routes it to a Stripe account, creates the PaymentIntent there and returns a hosted checkout URL on your payment domain.

Request body

FieldTypeDescription
amountrequired number Decimal major units. Minimum 0.50.
currency string Three-letter ISO code. Defaults to "usd".
customer_emailrequired string Shopper email. Used for the receipt and shown on the checkout.
customer_name string Shopper name.
description string Up to 500 characters. Appears on the checkout and in the dashboard.
metadata object Up to 20 string keys. Returned unchanged in GET responses and webhooks.
return_urlrequired string URL the shopper is sent to after paying. See Return redirect.
webhook_url string Per-transaction webhook endpoint. Receives the same signed events as your account webhook.
payment_account_id integer Bypass routing and use this account. Must be one of your active accounts (see Payment accounts).

Example request

curl -X POST http://orcapayz.com/api/v1/payment-intents \
  -H "X-API-Key: $ORCA_KEY" \
  -H "X-API-Secret: $ORCA_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 49.90,
    "currency": "usd",
    "customer_email": "jane@example.com",
    "customer_name": "Jane Doe",
    "description": "Order #10422",
    "metadata": { "order_id": "10422" },
    "return_url": "https://yourstore.com/thank-you"
  }'

Response 201 Created

{
  "success": true,
  "data": {
    "transaction_id": "txn_9f2c1a7e3b…",
    "checkout_url": "https://pay.yourdomain.com/checkout/txn_9f2c1a7e3b…",
    "status": "processing",
    "amount": 49.9,
    "currency": "usd",
    "payment_intent": {
      "id": "pi_3Q…",
      "client_secret": "pi_3Q…_secret_…"
    },
    "payment_account": {
      "id": 3,
      "publishable_key": "pk_live_…"
    }
  }
}

Recommended: redirect the shopper to checkout_url. The hosted checkout carries your branding, mounts the Stripe Payment Element under your domain and handles 3D Secure. Alternative: build your own form with Stripe Elements using client_secret and the account's publishable_key. In that case you must confirm the intent yourself and use return_url in your Stripe confirm call.

Possible errors: 422 validation, 400 NO_AVAILABLE_ACCOUNT, 400 ACCOUNT_LIMIT_EXCEEDED, 403 INVALID_PAYMENT_ACCOUNT, 502 PROVIDER_ERROR.

Return redirect

After the shopper completes (or fails) payment on the hosted checkout they are redirected to your return_url with these query parameters appended:

https://yourstore.com/thank-you
  ?transaction_id=txn_9f2c1a7e3b…
  &status=succeeded
  &payment_intent=pi_3Q…
  &redirect_status=succeeded
  • transaction_id — the OrcaPayz transaction.
  • status — our status at redirect time: succeeded, processing or failed.
  • payment_intent — the Stripe PaymentIntent id.
  • redirect_status — the value Stripe appended after confirmation.

Never trust the redirect alone.

Query strings can be replayed or edited. Mark an order paid only after a payment_intent.succeeded webhook or a GET /api/v1/payment-intents/{transaction_id} returns status: "succeeded".

List payment intents

GET /api/v1/payment-intents

Returns your transactions across all accounts, newest first.

Query parameters

ParameterDescription
status One of pending, processing, succeeded, failed, refunded, cancelled.
customer_email Exact match on the shopper email.
from_date ISO 8601 date or datetime. Inclusive lower bound on created_at.
to_date ISO 8601 date or datetime. Inclusive upper bound on created_at.
limit 1 to 100. Default 50.
offset Number of rows to skip. Default 0.

Response

{
  "success": true,
  "data": [
    {
      "transaction_id": "txn_9f2c1a7e3b…",
      "payment_intent_id": "pi_3Q…",
      "amount": 49.9,
      "refunded_amount": 0,
      "currency": "usd",
      "status": "succeeded",
      "customer_email": "jane@example.com",
      "customer_name": "Jane Doe",
      "description": "Order #10422",
      "paid_at": "2026-09-27T10:16:04+00:00",
      "created_at": "2026-09-27T10:15:32+00:00"
    }
  ],
  "count": 1,
  "total": 1284
}

count is the number of rows in this page; total is the number matching your filters.

Retrieve a payment intent

GET /api/v1/payment-intents/{transaction_id}

Returns the same fields as the list endpoint plus metadata, return_url, webhook_url, payment_account_id, checkout_url, error_message and updated_at. This is the endpoint to use for reconciliation.

{
  "success": true,
  "data": {
    "transaction_id": "txn_9f2c1a7e3b…",
    "payment_intent_id": "pi_3Q…",
    "amount": 49.9,
    "refunded_amount": 0,
    "currency": "usd",
    "status": "succeeded",
    "customer_email": "jane@example.com",
    "customer_name": "Jane Doe",
    "description": "Order #10422",
    "metadata": { "order_id": "10422" },
    "return_url": "https://yourstore.com/thank-you",
    "webhook_url": null,
    "payment_account_id": 3,
    "checkout_url": "https://pay.yourdomain.com/checkout/txn_9f2c1a7e3b…",
    "error_message": null,
    "paid_at": "2026-09-27T10:16:04+00:00",
    "created_at": "2026-09-27T10:15:32+00:00",
    "updated_at": "2026-09-27T10:16:04+00:00"
  }
}

Cancel a payment intent

POST /api/v1/payment-intents/{transaction_id}/cancel

Cancels a transaction that is still pending or processing. The Stripe PaymentIntent is cancelled and the checkout URL stops accepting payment. No request body.

{
  "success": true,
  "data": {
    "transaction_id": "txn_9f2c1a7e3b…",
    "status": "cancelled",
    "updated_at": "2026-09-27T10:20:11+00:00"
  }
}

Any other status returns 400 CANNOT_CANCEL. To reverse a succeeded payment, issue a refund instead.

Refund a payment intent

POST /api/v1/payment-intents/{transaction_id}/refund

Refunds a succeeded payment on the Stripe account that processed it. Partial refunds can be repeated until the full amount is returned.

Request body

FieldTypeDescription
amount number Optional. Decimal major units. Omit to refund the full remaining balance.
reason string Optional. One of duplicate, fraudulent, requested_by_customer.
curl -X POST http://orcapayz.com/api/v1/payment-intents/txn_9f2c1a7e3b…/refund \
  -H "X-API-Key: $ORCA_KEY" \
  -H "X-API-Secret: $ORCA_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 10.00, "reason": "requested_by_customer" }'

Response

{
  "success": true,
  "data": {
    "transaction_id": "txn_9f2c1a7e3b…",
    "refund_id": "re_3Q…",
    "amount": 10,
    "refunded_total": 10,
    "status": "succeeded"
  }
}

status becomes refunded once refunded_total equals the original amount. Failures return 400 REFUND_FAILED with Stripe's message. A charge.refunded webhook follows each successful refund.

Payment accounts

GET /api/v1/payment-accounts

Lists the Stripe accounts connected to your merchant account. Use id as payment_account_id when you want to bypass routing. Secret keys are never returned.

{
  "success": true,
  "data": [
    {
      "id": 3,
      "name": "EU store",
      "website": "https://yourstore.com",
      "payment_domain": "pay.yourstore.com",
      "domain_verified": true,
      "is_active": true,
      "priority": 1,
      "weight_percentage": 60
    },
    {
      "id": 4,
      "name": "US store",
      "website": "https://yourstore.com",
      "payment_domain": "pay.yourstore.com",
      "domain_verified": true,
      "is_active": true,
      "priority": 2,
      "weight_percentage": 40
    }
  ]
}

Accounts are managed in Dashboard → Payment accounts: keys, payment domain, daily and monthly limits, priority, weight and active state.

Routing strategies

The strategy is set once per merchant in Dashboard → Payment accounts and applies to every payment intent that does not pass payment_account_id. Before the strategy runs, the router removes accounts that are inactive, have an unverified domain, or would exceed their daily or monthly limit with this amount.

StrategyKeyBehaviour
Weighted Round RobinRecommended weighted_round_robin Distributes by target percentage. Only successful payments count.
Round Robin round_robin Cycles through accounts in order.
Weighted weighted Priority plus success rate. Prefers reliable accounts.
Least Used least_used Routes to the account with the lowest usage.
Failover failover Primary first, then secondaries.
Random random Random selection among eligible accounts.

Weighted Round Robin uses each account's weight_percentage as a target share and counts only succeeded payments, so failed attempts do not skew distribution. Failover orders accounts by priority (lowest number first).

Manual account selection

Pass payment_account_id in the create request to force a specific account. The router is skipped entirely, but the account must be yours and active; otherwise you get 403 INVALID_PAYMENT_ACCOUNT. Daily and monthly limits still apply and can return 400 ACCOUNT_LIMIT_EXCEEDED.

{
  "amount": 120.00,
  "currency": "eur",
  "customer_email": "jane@example.com",
  "return_url": "https://yourstore.com/thank-you",
  "payment_account_id": 3
}

Webhooks

OrcaPayz sends a POST with a JSON body to your account-level webhook URL (Dashboard → Webhooks) and, when set, to the per-transaction webhook_url. Delivery logs are visible in Dashboard → Webhook logs.

Events

  • payment_intent.succeeded — the payment was captured.
  • payment_intent.payment_failed — the attempt failed; error_message explains why.
  • payment_intent.canceled — the intent was cancelled via API or expired.
  • charge.refunded — a full or partial refund settled; adds fully_refunded.
  • ping — sent by the test button in the dashboard.

Payload

{
  "event": "payment_intent.succeeded",
  "data": {
    "transaction_id": "txn_9f2c1a7e3b…",
    "payment_intent_id": "pi_3Q…",
    "amount": 49.9,
    "refunded_amount": 0,
    "currency": "usd",
    "status": "succeeded",
    "customer_email": "jane@example.com",
    "customer_name": "Jane Doe",
    "description": "Order #10422",
    "metadata": { "order_id": "10422" },
    "error_message": null,
    "paid_at": "2026-09-27T10:16:04+00:00",
    "created_at": "2026-09-27T10:15:32+00:00"
  },
  "timestamp": "2026-09-27T10:16:05+00:00"
}

For charge.refunded the data object also contains fully_refunded (boolean) and an updated refunded_amount.

Delivery

  • Respond with any 2xx within a few seconds. Do your heavy work asynchronously.
  • Retries are not automatic. If your endpoint was down, reconcile with GET /api/v1/payment-intents filtered by from_date.
  • Events may occasionally arrive out of order. Use status and timestamp, not arrival order.
  • Handlers should be idempotent on transaction_id + event.

Verifying signatures

Every webhook carries an X-Orcapayz-Signature header: the lowercase hex HMAC-SHA256 of the raw request body, keyed with your webhook secret from Dashboard → Webhooks. Compute the same digest over the exact bytes you received (before JSON parsing) and compare with a constant-time function.

X-Orcapayz-Signature: 3f1a9c…e2b7
Content-Type: application/json
User-Agent: OrcaPayz-Webhooks/1.0
<?php

$payload   = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_ORCAPAYZ_SIGNATURE'] ?? '';
$expected  = hash_hmac('sha256', $payload, getenv('ORCA_WEBHOOK_SECRET'));

if (! hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$event = json_decode($payload, true);

switch ($event['event']) {
    case 'payment_intent.succeeded':
        // Mark order $event['data']['metadata']['order_id'] as paid.
        break;
    case 'payment_intent.payment_failed':
        // Notify the customer; see $event['data']['error_message'].
        break;
    case 'charge.refunded':
        // $event['data']['refunded_amount'], $event['data']['fully_refunded']
        break;
}

http_response_code(200);
import crypto from 'node:crypto';
import express from 'express';

const app = express();

app.post(
  '/webhooks/orcapayz',
  express.raw({ type: 'application/json' }), // keep the raw body
  (req, res) => {
    const expected = crypto
      .createHmac('sha256', process.env.ORCA_WEBHOOK_SECRET)
      .update(req.body)
      .digest('hex');
    const received = req.get('X-Orcapayz-Signature') || '';

    const valid =
      expected.length === received.length &&
      crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));

    if (!valid) return res.sendStatus(401);

    const event = JSON.parse(req.body.toString('utf8'));

    if (event.event === 'payment_intent.succeeded') {
      // Mark order event.data.metadata.order_id as paid.
    }

    res.sendStatus(200);
  }
);

Code examples

Create a payment intent and redirect the shopper to the hosted checkout. Replace the environment variables with the key pair from your dashboard.

curl -X POST http://orcapayz.com/api/v1/payment-intents \
  -H "X-API-Key: $ORCA_KEY" \
  -H "X-API-Secret: $ORCA_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 49.90,
    "currency": "usd",
    "customer_email": "jane@example.com",
    "return_url": "https://yourstore.com/thank-you",
    "metadata": { "order_id": "10422" }
  }'
<?php

$ch = curl_init('http://orcapayz.com/api/v1/payment-intents');

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'X-API-Key: ' . getenv('ORCA_KEY'),
        'X-API-Secret: ' . getenv('ORCA_SECRET'),
        'Content-Type: application/json',
        'Accept: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'amount'         => 49.90,
        'currency'       => 'usd',
        'customer_email' => 'jane@example.com',
        'return_url'     => 'https://yourstore.com/thank-you',
        'metadata'       => ['order_id' => '10422'],
    ]),
]);

$json = json_decode(curl_exec($ch), true);
curl_close($ch);

if (! ($json['success'] ?? false)) {
    throw new RuntimeException($json['message'] ?? 'OrcaPayz request failed');
}

// Store $json['data']['transaction_id'] against the order, then redirect.
header('Location: ' . $json['data']['checkout_url'], true, 303);
exit;
const response = await fetch('http://orcapayz.com/api/v1/payment-intents', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.ORCA_KEY,
    'X-API-Secret': process.env.ORCA_SECRET,
    'Content-Type': 'application/json',
    'Accept': 'application/json',
  },
  body: JSON.stringify({
    amount: 49.90,
    currency: 'usd',
    customer_email: 'jane@example.com',
    return_url: 'https://yourstore.com/thank-you',
    metadata: { order_id: '10422' },
  }),
});

const json = await response.json();

if (!json.success) {
  throw new Error(json.message ?? 'OrcaPayz request failed');
}

// Store json.data.transaction_id against the order, then redirect.
res.redirect(303, json.data.checkout_url);
import os
import requests

response = requests.post(
    "http://orcapayz.com/api/v1/payment-intents",
    headers={
        "X-API-Key": os.environ["ORCA_KEY"],
        "X-API-Secret": os.environ["ORCA_SECRET"],
        "Accept": "application/json",
    },
    json={
        "amount": 49.90,
        "currency": "usd",
        "customer_email": "jane@example.com",
        "return_url": "https://yourstore.com/thank-you",
        "metadata": {"order_id": "10422"},
    },
    timeout=15,
)

body = response.json()

if not body["success"]:
    raise RuntimeError(body.get("message", "OrcaPayz request failed"))

# Store body["data"]["transaction_id"] against the order, then redirect.
checkout_url = body["data"]["checkout_url"]

Ready to make your first call?

Sign up, connect a Stripe account and create an API key pair. 7-day trial, no card required.