Paypal
Skill CleanSlice/skills/paypal
CleanSlice agent skills — architecture patterns, vertical slices, conventional commits for Claude Code and AI coding agents.
npx -y skills add CleanSlice/skills --skill paypalAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
PayPal Orders v2 API access via OAuth client_credentials. Two secrets per account (client_id + client_secret), exposed to agents as PAYPAL_CLIENT_ID / PAYPAL_CLIENT_SECRET. Server-side only — checkout buttons, Pay Later messaging, Apple/Google Pay belong in the storefront, not in an agent skill.
SKILL.md
7.8 KB, as published. Nobody here has run it
PayPal
Same setup mechanics as [[openai]] — secret-mechanism, vault-backed — but PayPal
needs two credentials (client_id + client_secret) and uses OAuth
client_credentials to mint a short-lived bearer token. This file covers what's
different from [[stripe]]: the OAuth dance, sandbox vs live separation, and
which mutations require explicit user confirmation.
Quick Reference
| Need | Answer |
|---|---|
| Mechanism | secret |
| Env vars | PAYPAL_CLIENT_ID + PAYPAL_CLIENT_SECRET |
| Per-account aliases | PAYPAL_CLIENT_ID_<ACCOUNTKEY> (e.g. _SANDBOX, _LIVE) |
| Base URLs | api-m.sandbox.paypal.com (sandbox) / api-m.paypal.com (live) |
| Token lifetime | ~9h (access_token from /v1/oauth2/token) |
| Where to create | developer.paypal.com → Apps & Credentials |
Catalogue note. The integration catalogue today exposes one
secretEnvKeyper service. Until that's extended, the recommended convention is two separate accountKeys on the same service:paypal:client_idandpaypal:secret. Resolve both viaintegration_secretsand pair them in code (see snippet below). The cleaner alternative is a small extension tocatalogue.tsand the connect dialog to accept two fields — file a ticket against the integration slice if you need it.
Setting up
- Create a sandbox app at developer.paypal.com → Apps & Credentials →
Sandbox → Create App. Copy
Client IDandSecret. - Open the admin UI at
/integrations→ PayPal → accountKeysandbox→ pasteclient_id. Repeat with accountKeysandbox-secret→ paste secret. - Verify the credentials by exchanging them for a token (see "Minting a token" below). Don't promote to live until at least one full create → approve → capture cycle works in sandbox.
- For production, repeat the steps with the live app credentials and use
accountKey
live/live-secret.
Sandbox and live are separate PayPal accounts — orders never cross. Keep them as distinct accountKeys so the agent has to pick deliberately.
Minting a token
The runtime resolves both secrets, you Base64 them, and trade for a bearer:
const { env } = await integration_secrets({ service: 'paypal' })
// Use the explicit account when both sandbox and live are connected.
// Fall back to the default alias (most-recently-updated) otherwise.
const clientId = env.PAYPAL_CLIENT_ID_SANDBOX ?? env.PAYPAL_CLIENT_ID
const clientSecret = env.PAYPAL_CLIENT_SECRET_SANDBOX ?? env.PAYPAL_CLIENT_SECRET
if (!clientId || !clientSecret) {
return ctx.send(
'PayPal isn\'t connected. Open /integrations and add a PayPal client_id + secret first.',
)
}
const base = clientId.startsWith('A') // live ids start with A (sandbox starts with A too — use accountKey to decide)
? 'https://api-m.paypal.com'
: 'https://api-m.sandbox.paypal.com'
const auth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
const tokenRes = await fetch(`${base}/v1/oauth2/token`, {
method: 'POST',
headers: {
Authorization: `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'grant_type=client_credentials',
})
const { access_token } = await tokenRes.json()
The base URL choice is not derivable from the client_id prefix — both
sandbox and live ids start with A. Pick the base from the accountKey
(sandbox → sandbox base, live → live base) and surface a clear error
if the agent is ambiguous about which account it wants.
Each tool call mints a fresh token. Don't try to cache across calls — the runtime doesn't give tools a per-user cache, and 9-hour tokens aren't worth the persistence work.
Calling the API
Orders v2 is the only API a runtime agent should touch directly. Everything else (Subscriptions, Payouts, Disputes) is either covered by Orders or needs human review.
// Create an order (intent: CAPTURE for one-step sale, AUTHORIZE for two-step).
const res = await fetch(`${base}/v2/checkout/orders`, {
method: 'POST',
headers: {
Authorization: `Bearer ${access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
intent: 'CAPTURE',
purchase_units: [{
reference_id: ref,
amount: { currency_code: 'USD', value: '100.00' },
description,
}],
}),
})
const order = await res.json()
// order.id e.g. "7MW20236HK..." — store it; orders expire after 3h.
Then surface order.id and the approval link (from order.links[].href where
rel === 'approve') to the user. The agent never approves the order itself
— the buyer does, in their PayPal session. Once approved, capture:
await fetch(`${base}/v2/checkout/orders/${order.id}/capture`, {
method: 'POST',
headers: { Authorization: `Bearer ${access_token}` },
})
Safety rails
Charging, capturing, and refunding are irreversible — wrap every mutation in an explicit user confirmation, same pattern as [[stripe]]:
await ctx.send(
`About to capture ${formatMoney(amount)} on PayPal order ${order.id}. Reply "confirm capture" to proceed.`,
)
// ... wait for the user's "confirm capture" message ...
await fetch(`${base}/v2/checkout/orders/${order.id}/capture`, {
method: 'POST',
headers: { Authorization: `Bearer ${access_token}` },
})
Mutating calls that always require confirmation:
POST /v2/checkout/orders/{id}/capture— charges the buyerPOST /v2/payments/captures/{id}/refund— issues a refundPOST /v2/payments/authorizations/{id}/void— voids an authorizationPOST /v1/payments/payouts— sends money out
Read-only flows (GET /v2/checkout/orders/{id}, listing transactions for a
date range) don't need confirmation, but watch out for date filters that
return huge result sets — paginate, don't dump.
3D Secure / SCA is mostly a frontend concern — the buyer's PayPal session handles the challenge during approval. The agent doesn't see it.
What's out of scope for this skill
These belong to the storefront/frontend, not a runtime agent — don't try to script them via this skill:
- JavaScript SDK button rendering (
paypal-js,paypal-buttons) - Pay Later messaging widgets, Apple Pay / Google Pay buttons
- Fastlane / Express Checkout (frontend-only flow)
- Card Fields (Expanded Checkout PCI-handled iframe)
- Webhook receivers — those need a public HTTPS endpoint; if you need them, add a server slice in the host app and surface events to the agent via internal API, don't try to host them inside the runtime
Subscriptions (/v1/billing/subscriptions) and Payouts work via the same
OAuth token if the agent really needs them, but both require human review
flows that aren't standardized yet — confirm with the user before adding.
Don't
- Don't promote sandbox credentials to live by editing the accountKey — always re-paste from the live app. Live secrets are not derivable from sandbox ones.
- Don't log requests with the
Authorizationheader or the rawclient_secret/access_token. Filter before logging. - Don't fire captures, refunds, or payouts from autonomous loops — always loop in the user, even for "small" amounts.
- Don't reuse an order id across sessions. PayPal orders expire after 3h and one-shot capture; create a fresh order for each buyer interaction.
- Don't infer environment from the client_id prefix — both sandbox and live
ids start with
A. Drive base URL from the accountKey. - Don't store webhook signing secrets in this integration row. They're a separate concern owned by the host app, not the user's PayPal account.