Powered by Arrowhead Advisory Group
Developers

REST API. 272 endpoints. One token.

Penumbra's API is REST/JSON, authenticated with a single bearer token. Routing decisions, transactions, disputes, ledger, webhooks, merchant boarding, settlement, payouts. All under api.penumbrahq.com/v1/. SDKs in six languages.

272
API endpoints
41
Webhook event types
sub-20ms
Decision latency
99.9%
Webhook delivery SLO

Quickstart

Get your first routing decision back in under five minutes. You'll need a Penumbra account and a sandbox API key from the dashboard.

1
Grab a sandbox key. Sign in, open Settings → API keys, generate an sk_test_… key. Revoke and rotate from the same screen.
2
POST to /v1/routing/decide with a transaction shape. You'll get back the winning processor, the runner-up, and the reasoning.
3
Submit the charge. Use the returned processor to call the right downstream PSP. Or let Penumbra submit it for you with POST /v1/routing/execute.
4
Subscribe to webhooks. Point a URL at us. We'll publish the transaction.authorized / captured / refunded / etc. events in the Pen.v1 schema.
5
Flip to live. Swap sk_test_ for sk_live_. No code changes. Webhook URLs and signing secrets are per-environment so the cutover is clean.

Below: the same first request, in six languages.

curl https://api.penumbrahq.com/v1/routing/decide \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount_cents": 124700,
    "currency": "USD",
    "card_bin": "424242",
    "mcc": "5812",
    "merchant_id": "mer_a4b9c2e1"
  }'
import Penumbra from '@penumbra/node';

const penumbra = new Penumbra(process.env.PENUMBRA_KEY);

const decision = await penumbra.routing.decide({
  amount_cents: 124700,
  currency: 'USD',
  card_bin: '424242',
  mcc: '5812',
  merchant_id: 'mer_a4b9c2e1',
});

console.log(decision.chosen.processor); // 'adyen'
import penumbra
import os

penumbra.api_key = os.environ["PENUMBRA_KEY"]

decision = penumbra.Routing.decide(
    amount_cents=124700,
    currency="USD",
    card_bin="424242",
    mcc="5812",
    merchant_id="mer_a4b9c2e1",
)

print(decision.chosen.processor)  # 'adyen'
import "github.com/penumbrahq/penumbra-go"

client := penumbra.NewClient(os.Getenv("PENUMBRA_KEY"))

decision, err := client.Routing.Decide(ctx, &penumbra.DecideParams{
  AmountCents: 124700,
  Currency:    "USD",
  CardBIN:     "424242",
  MCC:         "5812",
  MerchantID:  "mer_a4b9c2e1",
})
require "penumbra"

Penumbra.api_key = ENV["PENUMBRA_KEY"]

decision = Penumbra::Routing.decide(
  amount_cents: 124700,
  currency:     "USD",
  card_bin:     "424242",
  mcc:          "5812",
  merchant_id:  "mer_a4b9c2e1",
)
require_once 'vendor/autoload.php';

\Penumbra\Penumbra::setApiKey(getenv('PENUMBRA_KEY'));

$decision = \Penumbra\Routing::decide([
  'amount_cents' => 124700,
  'currency'     => 'USD',
  'card_bin'     => '424242',
  'mcc'          => '5812',
  'merchant_id'  => 'mer_a4b9c2e1',
]);

SDKs & libraries

Officially maintained client libraries. All track the same API surface; new endpoints land in every SDK within one release.

JS@penumbra/node
PYpenumbra-python
GOpenumbra-go
RBpenumbra-ruby
PHPpenumbra-php
JVpenumbra-java

Authentication

Every request carries a bearer token in the Authorization header. Tokens are environment-scoped, scope-restricted, and rotatable.

Key types

PrefixEnvironmentUse
sk_test_…SandboxFull API access against simulated processors. No real money moves.
sk_live_…ProductionReal charges to real processors. Treat as the password to your money.
pk_…PublishableClient-side tokenization only. Cannot create charges or move money.
rk_…RestrictedCustom-scoped (e.g. read-only, reports-only). Created in dashboard.

Rotation

Rotate from Settings → API keys. Revocations propagate to all edge nodes in under 60 seconds. We recommend rotating live keys quarterly and immediately after any suspected exposure.

Errors & retries

Standard HTTP status codes. 2xx success, 4xx client error, 5xx server error. Every error body has a type, a human-readable message, and a request_id you can quote to support.

{
  "error": {
    "type": "invalid_request_error",
    "code": "missing_card_bin",
    "message": "card_bin is required for card-not-present routing decisions",
    "request_id": "req_8f3d2a17b4c19",
    "doc_url": "https://penumbrahq.com/docs/errors#missing_card_bin"
  }
}

Safe to retry: any 5xx, plus 429 (rate limit) and 409 (idempotency conflict mid-write). Use exponential backoff starting at 250ms, max 30s.

Not safe to retry without idempotency keys: any 2xx followed by a network error. The request may have succeeded server-side. Always include an Idempotency-Key header on write endpoints.

Rate limits

Per-key, sliding-window. Limits scale with tier; defaults below.

Endpoint classSandboxLive (Pro)Live (Scale+)
Routing decisions25 req/s100 req/s1,000 req/s
Read endpoints50 req/s200 req/s2,000 req/s
Write endpoints (non-routing)10 req/s50 req/s500 req/s
Webhook subscribe/configure5 req/s10 req/s20 req/s

Limits surface in response headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Exceed the limit and you get a 429 with a Retry-After header in seconds.

Idempotency

Every write endpoint accepts an Idempotency-Key header. Send the same key with the same payload and we'll return the cached response — never double-charge, never double-publish. Keys are remembered for 24 hours.

curl https://api.penumbrahq.com/v1/routing/execute \
  -H "Authorization: Bearer sk_live_..." \
  -H "Idempotency-Key: order_a4b9c2e1_attempt_1" \
  -d '{...}'

UUIDs work; opaque order IDs work better because they're deterministic if you retry from your application code.

Routing

The routing engine scores every connected processor on approval probability, latency, and cost. Submit a transaction shape and get a ranked recommendation in milliseconds. Or have us submit it for you.

POST /v1/routing/decidePOST /v1/routing/executeGET /v1/routing/policiesPATCH /v1/routing/policies/{id}

Decide response

{
  "decision_id": "dec_4Ag8nP2",
  "chosen": {
    "processor": "adyen",
    "approval_probability": 0.956,
    "expected_cost_bps": 241,
    "latency_ms_p50": 12
  },
  "runner_up": { "processor": "stripe", "approval_probability": 0.951 },
  "reason": "adyen_lower_cost_at_equivalent_approval",
  "policy_id": "pol_default"
}

Transactions

List, retrieve, and inspect every charge processed through Penumbra. Each transaction carries the original routing decision and the response from the processor.

GET /v1/transactionsGET /v1/transactions/{id}POST /v1/transactions/{id}/refundPOST /v1/transactions/{id}/capture

Filter by processor, status, amount range, time window, MCC, card brand. Each row exposes the routing decision_id so you can replay the decision against current policies.

Disputes

The disputes engine ingests carrier notifications, assembles evidence, drafts the rebuttal, and submits when you approve. Stripe, Adyen, Worldpay, NMI all supported as submission targets.

GET /v1/disputesGET /v1/disputes/{id}POST /v1/disputes/{id}/submitPOST /v1/disputes/{id}/accept

Ledger

Double-entry immutable ledger with full chain-of-custody. Every transaction, payout, fee, refund, reserve adjustment writes journal entries you can audit. Built on the same accounting primitives as the operator dashboard.

GET /v1/ledger/balanceGET /v1/ledger/entriesGET /v1/ledger/journal/{id}GET /v1/ledger/splits

Payouts

Schedule, inspect, or override payouts to merchants and to platform-fee accounts. ACH, Same-Day ACH, FedNow when sponsor-bank enrollment closes, and USDC settlement via Circle across five chains.

GET /v1/payoutsPOST /v1/payoutsPATCH /v1/payouts/{id}

Merchants

Submit boarding applications, retrieve KYB/risk decisions, manage MIDs across the processors a merchant boarded with. Penumbra holds a thin merchant-identity record; PSP underwriting decisions are passed through.

POST /v1/merchants/onboarding/initiateGET /v1/merchants/{id}GET /v1/merchants/{id}/mids

Webhooks

Subscribe to any of the 41 canonical Pen.v1 events. HMAC-SHA256 signed, retried with exponential backoff up to 24 hours, 99.9% delivery SLO.

POST /v1/webhooks/subscribeGET /v1/webhooksDELETE /v1/webhooks/{id}

Full event catalogue with payloads at Webhook events.

Try it in the sandbox right now.

Generate a sandbox key, hit the routing engine with your own payloads, watch the decisions come back. No sales call required.

Open the Sandbox