Announcing CIMD support for MCP Client registration
Learn more

PayPal MCP vs PayPal API for AI Agents (2026)

PayPal gives an AI agent two ways into a merchant's business account: an official MCP server, hosted and local, built on its open-source Agent Toolkit, and the REST API every checkout integration already uses. They overlap on invoices, orders, and refunds. They diverge on payouts, authorizations, dispute handling, events, and most of all on the credential model, because nearly every path into PayPal ends at a client ID and secret that can move a merchant's money. This guide compares what each path lets an agent do and how to hold those credentials for many merchants in production.

TL;DR

  • PayPal runs a remote MCP server at https://mcp.paypal.com (sandbox: https://mcp.sandbox.paypal.com) with SSE and Streamable HTTP transports, plus a local server, @paypal/mcp, that reads a PAYPAL_ACCESS_TOKEN. Both expose the PayPal Agent Toolkit: 43 tools in the toolkit's README covering invoices, orders, refunds, disputes, shipment tracking, catalog, subscriptions, and reporting. There are no payout tools, no authorization void or reauthorize, no balances, no dispute offers or escalation, and no event triggers. PayPal's docs give no GA or beta label.
  • The PayPal REST API authenticates with OAuth 2.0 Client Credentials: a REST app's client ID and secret go to /v1/oauth2/token and come back as a bearer token that lives about eight to nine hours, with no refresh token. The token acts as the merchant account that owns the app, not as a person. Scopes come from the app, not the request.
  • Acting for many merchants has two PayPal-sanctioned shapes: each merchant's own REST app credentials, or one platform credential plus a PayPal-Auth-Assertion header, which requires PayPal partner approval and each merchant's explicit consent. Either way, your app holds money-moving credentials for every merchant it serves.
  • MCP is the faster path for a merchant asking Claude to draft and send invoices, check a dispute, or pull last month's sales. The API is the path for headless agents that react to CUSTOMER.DISPUTE.CREATED webhooks, send payouts, release authorization holds, or reconcile balances.
  • Scalekit's paypal connector exposes 63 prebuilt tools over the PayPal REST API, authenticates as a service account with a PayPal REST app's client credentials, and keeps those credentials in the token vault per connected account, so the MCP vs API decision doesn't change your auth infrastructure.

The decision your PayPal agent forces first

Your agent needs to work inside your customers' PayPal business accounts: chase unpaid invoices, refund a damaged order, answer a buyer's dispute before the seller response deadline, add tracking numbers so payouts aren't held, or pay out a batch of contractors on Friday. PayPal ships an official Model Context Protocol (MCP) server, hosted and local, built on its open-source Agent Toolkit. PayPal also has the REST API every checkout integration already uses. They overlap on invoices, orders, and refunds. They diverge on payouts, authorizations, dispute handling, events, and, most of all, on the credential model, because nearly every path into PayPal ends at a client ID and secret that can move a merchant's money. Here's how to pick.

What PayPal MCP and PayPal API actually are

PayPal MCP

PayPal announced its MCP server on April 2, 2025 with invoicing as the first feature, offered both as a local server and as a remote server that "connects with a PayPal login." A June 2, 2025 update re-architected the remote server on the Agent Toolkit so it serves all the toolkit's tools, and restricted tool visibility so a client sees only the tools its token permits.

The MCP server docs list two remote endpoints, https://mcp.paypal.com for production and https://mcp.sandbox.paypal.com for sandbox, each with /sse and /http (Streamable HTTP) paths. Auth on the remote server is either a browser redirect to the PayPal login page, where you authorize the client, or an Authorization: Bearer header built from client ID and secret from the PayPal Developer Dashboard. The local server runs with npx -y @paypal/mcp --tools=all on Node.js 18 or later and takes PAYPAL_ACCESS_TOKEN plus PAYPAL_ENVIRONMENT set to SANDBOX or PRODUCTION. PayPal doesn't label either server GA or beta, and its docs warn that MCP and MCP clients "can come with performance issues or other challenges initially."

The Agent Toolkit README lists 43 tools, including create_invoice, send_invoice_reminder, create_recurring_series, create_order, pay_order, create_refund, list_disputes, accept_dispute_claim, create_shipment_tracking, create_subscription, cancel_subscription, list_transactions, and get_merchant_insights. PayPal's agent tools catalog shows a smaller 30-tool set plus three remote-only commerce tools, search_product, create_cart, and checkout_cart, which are limited to gift cards and need an x-feature-flags: commerce:true header. The catalog and the README don't match exactly, so check what your token actually returns from tools/list.

PayPal API

The PayPal REST API lives at https://api-m.paypal.com, with a sandbox at https://api-m.sandbox.paypal.com. It covers Orders v2, Payments v2 (authorizations, captures, refunds), Invoicing v2, Payouts, Disputes, Subscriptions and Catalog Products, Transaction Search, Balances, Add Tracking, Partner Referrals, and Webhooks Management. Authentication is OAuth 2.0 Client Credentials: you send the REST app's client ID and secret as HTTP Basic auth with grant_type=client_credentials to /v1/oauth2/token, and every API call carries the returned bearer token. Sandbox and live apps have separate credentials, created under Apps & Credentials in the developer dashboard.

Comparing them where it matters for agents

What your agent can actually do

Capability
PayPal MCP (mcp.paypal.com)
PayPal API (api-m.paypal.com)
Create and capture checkout orders
Yes, create_order, get_order, pay_order
Yes, Scalekit paypal_order_create, paypal_order_capture, paypal_order_update
Authorize now, capture or void later
No authorization tools
Yes, Scalekit paypal_order_authorize, paypal_authorization_capture, paypal_authorization_void, paypal_authorization_reauthorize
Refunds
Yes, create_refund, get_refund
Yes, Scalekit paypal_capture_refund with an idempotent request_id, paypal_refund_get
Invoicing
Yes, 20 tools, including recurring series, conditional rules, and delete_invoice
Yes, Scalekit paypal_invoice_create, paypal_invoice_send, paypal_invoice_remind, paypal_invoices_search, paypal_invoice_payment_record, templates; no recurring series
Disputes
Limited: list_disputes, get_dispute, and accept_dispute_claim only
Limited: Scalekit paypal_dispute_offer_make and paypal_dispute_escalate; no accept-claim or evidence tool
Payouts
None
Yes, Scalekit paypal_payout_create, paypal_payout_get, paypal_payout_item_cancel
Balances and transaction history
Limited: list_transactions; no balances
Yes, Scalekit paypal_balances_list, paypal_transactions_list (31-day windows, three years back)
Aggregated sales analytics
Yes, get_merchant_insights over a date range and interval
No insights endpoint; you aggregate transactions
Subscriptions and plans
Limited: create, show, update, cancel; no suspend or reactivate
Yes, Scalekit paypal_subscription_suspend, paypal_subscription_activate, paypal_subscription_revise, paypal_plan_pricing_update
Shipment tracking
Yes, create_shipment_tracking, update_shipment_tracking
Yes, Scalekit paypal_order_tracking_add, paypal_trackers_list, paypal_tracker_update
Event notifications
No triggers
Yes, Webhooks per REST app; not a connector tool

The pattern is clear. PayPal's MCP tools follow what a merchant does in a chat: draft an invoice, look something up, refund an order, concede a dispute. The API covers the money operations a back office runs without a chat window: holds, payouts, balances, dispute negotiation, and reactions to events.

What the MCP path makes easier

Two things. The first is invoicing depth. PayPal has invested most of the toolkit in invoices, and tools like create_recurring_series, create_conditional_rules_for_invoice, and cancel_invoice_auto_reminder let a merchant say "bill Globex $4,000 on the first of every month, with 2% off if they pay in ten days" without your team writing a schema for PayPal's nested invoice detail, amount breakdown, and payment term objects. Writing the schema is the hard part. Not the API call.

The second is aggregation. get_merchant_insights returns sales or order metrics over a date range and interval in one call. On the raw API, "how did Q3 compare to Q2" means paging GET /v1/reporting/transactions in 31-day windows, at most 500 rows a page, filtering event codes, and summing gross, fee, and net yourself, with each page landing in the model's context.

The tradeoff is that PayPal owns the surface. The README and the developer catalog already disagree on the count and on names (list_product versus list_products, list_transaction versus list_transactions), and which tools a client sees depends on the token. An agent tuned against today's names can change behavior without a release on your side.

The auth path each one puts you on

On the MCP path, auth depends on which server you pick. The remote server either sends the user through a PayPal login and authorization page, which gives a person-level session inside the MCP client, or takes a bearer header built from REST app client credentials. The local server takes a raw access token in PAYPAL_ACCESS_TOKEN, and the toolkit README says that token is valid for three to eight hours in sandbox and eight hours in production, with a recommendation to "implement token refresh logic before expiration." A local MCP config with a pasted token stops working the same day.

On the direct API path, there is one grant for your own account: Client Credentials. PayPal's sample token responses carry expires_in values of 31,668 and 32,400 seconds, roughly eight to nine hours, and a scope string listing what the app may call: invoicing, disputes, payments, refunds, payouts, subscriptions, vault, and webhooks among them. There is no refresh token; you request a new access token with the same credentials. PayPal's rate limiting page tells you not to generate a new token for each transaction and to cache it instead.

The identity is the catch. A Client Credentials token acts as the merchant account that owns the REST app. Every refund, payout, and dispute offer is attributed to that app, not to the employee who asked the agent for it. PayPal has no per-user scope inside a business account on this path, so the line between what a support agent may refund and what a finance lead may pay out is yours to draw.

Acting for other merchants adds two PayPal-specific shapes. In a first-party integration, you onboard each seller through POST /v2/customer/partner-referrals, exchange the returned authCode and sharedId for the seller's token, and then retrieve the seller's client_id and client_secret from the partner's merchant-integrations/credentials endpoint under /v1/customer/partners/. Partner Referrals requires approved-partner status. The other shape is a single platform credential plus a PayPal-Auth-Assertion header, an unsigned JWT carrying your client_id as iss and the merchant's payer_id, which "requires explicit consent from each merchant you represent" and must travel with a PayPal-Partner-Attribution-Id header. Both shapes end with you holding a credential that can move each merchant's money.

Both paths still need per-merchant isolation

In a multi-tenant B2B agent, MCP gives you a credential per user and the direct API gives you a credential per user. In neither case does the path itself solve storage, rotation, or revocation; those are infrastructure problems regardless of which path you choose.

PayPal sharpens the stakes. A leaked OAuth refresh token for a calendar is a privacy incident. A leaked PayPal client secret with the payouts feature enabled is a funds incident, and it stays valid until someone rotates it in the developer dashboard. Your app has to bind each set of REST app credentials to exactly one customer, keep sandbox and live credentials in separate environments, and make sure no code path lets one tenant's agent run with another tenant's secret.

Recommended reading: access control for multi-tenant AI agents

What you own in production

On the MCP path, PayPal hosts the remote server, owns the tool schemas, and filters tools by token. You own each merchant's session or token inside an MCP client you may not control, and you own the gaps. MCP tools only run when someone starts a conversation, so nothing happens when a dispute opens or a payment is reversed. On the local server you also own the token: something has to mint a new PAYPAL_ACCESS_TOKEN every eight hours and restart or reconfigure the client.

On the API path, you own schemas, pagination, retries, and the token lifecycle. PayPal states that "we do not publish a rate limiting policy," returns 429 with RATE_LIMIT_REACHED when it throttles, and asks integrations not to poll for status. Transaction search has its own limits: date ranges of at most 31 days, history back three years, and new transactions that can take up to three hours to appear. Money-moving POST calls should carry a PayPal-Request-Id, which PayPal keeps for up to 45 days so a retry returns the original result instead of refunding twice; the Scalekit tools expose it as request_id on paypal_capture_refund, paypal_payout_create, paypal_order_create, and paypal_authorization_void.

Webhooks replace polling, with rules. Subscriptions belong to a REST app, so in a per-merchant-app model each merchant's app needs its own subscription and its own webhook_id. Your listener must return a 2xx, or PayPal retries up to 25 times over three days. You verify each delivery either by posting it back to /v1/notifications/verify-webhook-signature or by checking the paypal-transmission-sig header yourself against PayPal's certificate, over a string built from the transmission ID, transmission time, your webhook_id, and the CRC32 of the raw body. Either way you need the raw, unparsed body.

When to use MCP, when to use the API

Use PayPal MCP when:

  • A freelancer or small business owner in Claude asks "send Globex an invoice for 10 hours of consulting at $150, net 30" and wants create_invoice and send_invoice handled without a form.
  • A merchant wants a quick read on the business, such as "how did September sales compare to August," and get_merchant_insights answers in one call.
  • A support lead reviews open disputes with list_disputes and get_dispute and decides, case by case, to concede small ones with accept_dispute_claim.
  • You are prototyping a PayPal assistant for your own account in sandbox at https://mcp.sandbox.paypal.com, with a person approving every write.

Use the PayPal API when:

  • The agent runs headless: a CUSTOMER.DISPUTE.CREATED webhook triggers it to read the dispute with paypal_dispute_get and propose a refund with paypal_dispute_offer_make before the seller response deadline.
  • The workflow moves money on a schedule: a weekly paypal_payout_create batch to contractors after paypal_balances_list confirms available funds, followed by paypal_payout_get until every item settles.
  • The checkout flow authorizes first and captures on shipment, so the agent needs paypal_authorization_capture, paypal_authorization_reauthorize, and paypal_authorization_void when an order is cancelled.
  • You serve many merchants and need credentials per merchant, webhooks per merchant, idempotency keys on every write, and reconciliation through paypal_transactions_list.

The credential problem that exists on both paths

Both paths hand you a PayPal credential per merchant. Neither hands you a vault. A multi-tenant agent serving 120 merchants holds 120 REST app client ID and secret pairs on the API path, each minting a token that expires roughly every eight hours, each able to refund, invoice, and in many cases pay out. Behind them may sit your own platform app, whose secret anchors every PayPal-Auth-Assertion you send. Every secret must be stored encrypted, kept out of logs and LLM context, exchanged for a cached token rather than a fresh one per call, rotated when a merchant suspects exposure, and deleted when a merchant offboards.

The MCP path changes the shape, not the problem. Login-based sessions live in each merchant's MCP client, the local server needs a fresh access token pasted in every eight hours, and the bearer option on the remote server is the same client credential again, now sitting in a desktop config file. The tempting shortcut is one platform-level credential for everyone. That gives you one secret to protect and a blast radius the size of your entire merchant base.

Scalekit's paypal connector holds each merchant's REST app credentials in the token vault as a connected account and handles authentication on every call, so the MCP vs API decision doesn't change your auth infrastructure. Your agent passes an identifier; credentials never touch the agent runtime or the LLM context.

Recommended reading: OAuth vs API keys for AI agents

How to connect a PayPal agent with Scalekit

Scalekit's paypal connector is an API-path connector built by Scalekit with 63 tools: 22 read, 41 write, and 0 destructive. It covers orders, authorizations, captures, refunds, invoices and invoice templates, disputes, payouts, catalog products, billing plans, subscriptions, shipment trackers, balances, and transaction search. It authenticates as a service account using a PayPal REST app's client credentials, so there is no consent screen and no refresh token to babysit: the connected account holds the app's client ID and secret, and Scalekit handles authentication for every call.

Prerequisites and the connection name

In the Scalekit dashboard, go to AgentKit > Connections and create a PayPal connection. The name you give the connection is the connection_name your code passes, and the string must match the dashboard exactly. This is the most common integration error. Copy SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, and SCALEKIT_CLIENT_SECRET from Developers > API Credentials into .env.

Each merchant needs a PayPal REST app. In the PayPal developer dashboard under Apps & Credentials, the merchant creates an app and copies its Client ID and Secret. Sandbox and live apps are separate; keep sandbox credentials in a Scalekit development environment and live credentials in production. Enable only the features the agent needs on the app, because the token PayPal issues carries every scope the app has. The connector docs list each tool's inputs.

pip install scalekit-sdk-python python-dotenv langchain-openai requests cryptography

Connect each merchant once

get_or_create_connected_account returns the connected account for an identifier. If it is not ACTIVE, get_authorization_link returns a Scalekit-hosted link; for a credential-based connector like PayPal it collects the credentials the connection requires instead of opening an OAuth consent screen. The merchant's PayPal admin enters the REST app credentials once, Scalekit stores them in the token vault, and the agent never sees them. The identifier is your customer's ID, because the credential covers the whole PayPal business account.

import os from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions CONNECTION_NAME = "paypal" # must match the connection name in AgentKit > Connections identifier = "merchant_globex" # your app's stable ID for this merchant's PayPal account account = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=identifier, ) if account.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=CONNECTION_NAME, identifier=identifier, ) print("The merchant's PayPal admin should open:", link.link) input("Press Enter after the PayPal REST app credentials are submitted...") account = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=identifier, ) if account.connected_account.status != "ACTIVE": raise RuntimeError(f"PayPal is {account.connected_account.status}, not ACTIVE")

Retrieve the PayPal tools this account can call

This is not a catalog load. The agent asks for the tools this connected account is authorized to call, then narrows them to the six a reconciliation agent needs. Loading all 63 would put paypal_payout_create, paypal_capture_refund, and paypal_subscription_cancel in front of a model whose job is to explain where the money went. One note on labels: paypal_invoices_search is classed as a write tool because PayPal's invoice search is a POST, but it changes nothing. None of the six below moves money.

tools = actions.langchain.get_tools( identifier=identifier, connection_names=[CONNECTION_NAME], tool_names=[ "paypal_balances_list", "paypal_transactions_list", "paypal_capture_get", "paypal_disputes_list", "paypal_invoices_search", "paypal_invoice_get", ], page_size=100, ) tool_map = {t.name: t for t in tools} print(sorted(tool_map))

Run the agent loop

The loop runs until the model stops requesting tools. Every call executes with this merchant's PayPal credentials, so the system prompt pins the agent to reading and to PayPal's 31-day search window.

from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage from langchain_openai import ChatOpenAI llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [ SystemMessage( "You reconcile one merchant's PayPal account. Use paypal_balances_list for " "current funds and paypal_transactions_list for activity; its start_date and " "end_date must be ISO 8601 with seconds and a time zone, at most 31 days apart. " "Use paypal_disputes_list with dispute_state REQUIRED_ACTION for disputes that " "need the seller, and paypal_invoices_search with status SENT or UNPAID for " "open invoices. Report amounts with their currency. Never guess IDs." ), HumanMessage( "For 2026-09-01 to 2026-09-30: what is the available USD balance, total gross " "and fees on successful payments, which refunds went out, which disputes need " "a response, and which invoices over $500 are still unpaid?" ), ] while True: response = llm.invoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: tool = tool_map.get(tc["name"]) if tool: result = tool.invoke(tc["args"]) else: result = f"Unknown tool: {tc['name']}" messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))

Deterministic calls with execute_tool

For pipelines, skip the model. execute_tool runs one tool with a fixed input and returns data plus an execution_id you can log. This handler runs when your webhook endpoint receives a CUSTOMER.DISPUTE.CREATED event. It verifies the PayPal signature against PayPal's certificate, reads the dispute, and if the dispute is still in the inquiry stage, is $25 or less, and allows an offer, it proposes a full refund. Anything else goes to a person.

import base64 import json import zlib from decimal import Decimal from urllib.parse import urlparse import requests from cryptography import x509 from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding AUTO_OFFER_LIMIT = Decimal("25.00") _cert_cache: dict[str, x509.Certificate] = {} def verify_paypal_webhook(raw_body: bytes, headers: dict, webhook_id: str) -> bool: """Self-verify a PayPal webhook. Header keys are expected in lowercase.""" cert_url = headers["paypal-cert-url"] parsed = urlparse(cert_url) host = parsed.hostname or "" if parsed.scheme != "https" or not (host == "paypal.com" or host.endswith(".paypal.com")): return False if cert_url not in _cert_cache: resp = requests.get(cert_url, timeout=10) resp.raise_for_status() _cert_cache[cert_url] = x509.load_pem_x509_certificate(resp.content) cert = _cert_cache[cert_url] crc = zlib.crc32(raw_body) & 0xFFFFFFFF message = ( f"{headers['paypal-transmission-id']}|{headers['paypal-transmission-time']}|" f"{webhook_id}|{crc}" ).encode() signature = base64.b64decode(headers["paypal-transmission-sig"]) try: cert.public_key().verify(signature, message, padding.PKCS1v15(), hashes.SHA256()) return True except InvalidSignature: return False def handle_dispute_created(raw_body: bytes, headers: dict, identifier: str) -> None: webhook_id = os.environ["PAYPAL_WEBHOOK_ID"] # stored per merchant subscription if not verify_paypal_webhook(raw_body, headers, webhook_id): raise PermissionError("Invalid PayPal webhook signature") event = json.loads(raw_body) if event.get("event_type") != "CUSTOMER.DISPUTE.CREATED": return dispute_id = event["resource"]["dispute_id"] dispute = actions.execute_tool( tool_name="paypal_dispute_get", tool_input={"dispute_id": dispute_id}, connection_name=CONNECTION_NAME, identifier=identifier, ) data = dispute.data print(dispute.execution_id, dispute_id, data.get("reason"), data.get("status")) amount = data.get("dispute_amount") or {} value = Decimal(amount.get("value", "0")) currency = amount.get("currency_code", "USD") stage = data.get("dispute_life_cycle_stage") can_offer = "make_offer" in (data.get("allowed_response_options") or {}) if stage != "INQUIRY" or not can_offer or value > AUTO_OFFER_LIMIT: print(f"{dispute_id}: {currency} {value} at {stage}, routed to a person") return offer = actions.execute_tool( tool_name="paypal_dispute_offer_make", tool_input={ "dispute_id": dispute_id, "offer_type": "REFUND", "offer_amount_value": str(value), "currency_code": currency, "note": ( "Sorry this order didn't work out. We're offering a full refund " "to resolve this right away." ), }, connection_name=CONNECTION_NAME, identifier=identifier, ) print(offer.execution_id, offer.data)

If the buyer accepts, PayPal issues the refund itself. The webhook subscription is not a connector tool. Create it once per merchant app with POST /v1/notifications/webhooks using that merchant's credentials, which Scalekit can make on the connected account's behalf through proxied API calls, and store the returned webhook_id per merchant. PayPal retries failed deliveries for up to three days, so deduplicate on the event id before calling the handler. The PayPal MCP server has no equivalent for either step.

Two other writes show where the API path earns its place. paypal_authorization_void releases a hold on a cancelled order so the buyer's funds come back without a refund cycle, and paypal_payout_create sends a batch to contractors by email, phone, or Venmo handle; PayPal rejects a sender_batch_id reused within 30 days, and request_id makes retries safe. Neither belongs in a model's default tool set; both fit a pipeline with a human approval step in front of them.

Multi-tool, multi-tenant PayPal agents on a virtual MCP server

The paypal connector has 63 tools. At roughly 200 tokens each, that is about 12,600 tokens of tool definitions before the agent does any work. A model choosing among paypal_transactions_list, paypal_subscription_transactions_list, paypal_invoices_list, and paypal_invoices_search picks the wrong list more often than a model holding only the one it needs. Overreach matters more here than with most connectors: a dispute agent that also holds paypal_payout_create and paypal_capture_refund can send money on a bad inference. The fix is not better prompting. It is surface reduction. A Scalekit virtual MCP server declares exactly which tools an agent role can see, and session tokens keep each run bound to one merchant's connected accounts. Docs: set up a virtual MCP server.

Create the server once per agent role

from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping vmcp = actions.mcp.create_config( name="paypal-dispute-desk", description="Reads PayPal disputes, proposes refund offers, and posts a Slack summary", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="paypal", tools=[ "paypal_disputes_list", "paypal_dispute_get", "paypal_capture_get", "paypal_dispute_offer_make", ], ), McpConfigConnectionToolMapping( connection_name="slack", tools=["slack_send_message"], ), ], ) print(vmcp.config.id, vmcp.config.mcp_server_url)

Mint a session token per merchant, per run

One server definition serves every merchant. Before each run, check that the merchant's accounts are active and mint a short-lived session token scoped to them. Any MCP client that speaks Streamable HTTP can connect; this example uses langchain-mcp-adapters (pip install langchain-mcp-adapters) and passes the token as a Bearer header.

import asyncio from datetime import timedelta from langchain_mcp_adapters.client import MultiServerMCPClient config_id = vmcp.config.id mcp_server_url = vmcp.config.mcp_server_url merchant_id = "merchant_globex" accounts = actions.mcp.list_mcp_connected_accounts( config_id=config_id, identifier=merchant_id, include_auth_link=True, ) not_ready = [ a for a in accounts.connected_accounts if str(a.connected_account_status).upper() != "ACTIVE" ] for a in not_ready: print(f"{a.connection_name} is {a.connected_account_status}: {a.authentication_link}") if not_ready: raise RuntimeError("Connect the accounts above before starting this run") session_token = actions.mcp.create_session_token( mcp_config_id=config_id, identifier=merchant_id, expiry=timedelta(minutes=30), ).token async def load_tools(): client = MultiServerMCPClient( { "scalekit": { "transport": "streamable_http", "url": mcp_server_url, "headers": {"Authorization": f"Bearer {session_token}"}, } } ) return await client.get_tools() mcp_tools = asyncio.run(load_tools()) print([t.name for t in mcp_tools])

The agent sees five tools instead of 63. The endpoint is static; the identity is per merchant. Unlike PayPal's own MCP server, where tool visibility follows whatever the token permits, this endpoint is scoped by agent role: the dispute desk gets paypal_dispute_offer_make and never sees payouts, and a separate payouts role can hold paypal_payout_create without ever seeing disputes.

Observability: every PayPal action tied to a merchant

Every execute_tool call returns an execution_id, and every execution is attributed to a connected account, which maps to one of your merchants. When a merchant asks which agent run offered refunds on 14 disputes overnight or voided an authorization on Sunday, the answer is a lookup, not an investigation. PayPal keeps its own trail too: refunds, captures, and payouts show up in paypal_transactions_list, and custom_id and invoice_id on paypal_capture_refund let you stamp your own reference on each money movement.

Attribution is where Client Credentials bites. PayPal records every API call against the REST app, so from PayPal's side the support lead who asked for a refund and the pipeline that issued a payout look identical. Your execution log is the only place that records which of your users triggered which action. A shared platform credential across merchants weakens even that: every call traces back to one secret, and a leak exposes every merchant at once. Shared credentials work for a single user. They do not survive a second user.

Recommended reading: audit trails for agent auth

Which one to build against

If your agent is a merchant's assistant in Claude or another MCP client, drafting and sending invoices, pulling sales insights, and handling the occasional refund with a person watching, PayPal's official MCP server is a good fit: it is hosted, it runs in sandbox and production, and its invoicing tools go deeper than most teams would build. If your agent runs unattended, reacts to dispute and payment webhooks, holds and releases authorizations, sends payouts, or reconciles balances across many merchants, build against the PayPal REST API, where you choose the operations and attach an idempotency key to every write. Either way, you hold a Client Credentials secret per merchant that can move that merchant's money, and that's what needs production-grade infrastructure.

FAQs

Does PayPal have an official MCP server?

Yes. PayPal runs a remote MCP server at https://mcp.paypal.com, with a sandbox at https://mcp.sandbox.paypal.com, over SSE and Streamable HTTP, plus a local server, @paypal/mcp, that reads a PAYPAL_ACCESS_TOKEN. Both expose the PayPal Agent Toolkit, whose README lists 43 tools for invoices, orders, refunds, disputes, shipment tracking, catalog, subscriptions, and reporting. PayPal's docs give it no GA or beta label.

What can an agent not do through PayPal MCP?

There are no payout tools, no authorization void or reauthorize, no balances, no dispute offers or escalation, and no event triggers. Disputes are limited to listing, reading, and accepting a claim. Subscriptions can be created, shown, updated, and cancelled but not suspended or reactivated. The README and PayPal's developer catalog also disagree on tool count and names, so check what your token returns from tools/list.

Whose identity does a PayPal API token carry?

A Client Credentials token acts as the merchant account that owns the REST app, not as a person. Every refund, payout, and dispute offer is attributed to that app, and PayPal has no per-user scope inside a business account on this path. Tokens live about eight to nine hours with no refresh token, and scopes come from the app's enabled features rather than the request.

How does an agent act for many PayPal merchants?

PayPal sanctions two shapes. In a first-party integration you onboard each seller through Partner Referrals and retrieve that seller's own client_id and client_secret, which requires approved-partner status. Alternatively you use one platform credential plus a PayPal-Auth-Assertion header carrying the merchant's payer_id, which requires explicit consent from each merchant. Both end with you holding a credential that can move each merchant's money.

When should an agent use the PayPal API instead of MCP?

When it reacts to CUSTOMER.DISPUTE.CREATED webhooks and proposes refunds before the seller response deadline, when it sends scheduled payouts after confirming balances, when the checkout flow authorizes first and captures on shipment, or when it serves many merchants and needs credentials per merchant, webhooks per merchant, idempotency keys on every write, and reconciliation through transaction search.

How does Scalekit connect an AI agent to PayPal?

The paypal connector exposes 63 tools over the PayPal REST API and authenticates as a service account with a REST app's client credentials, which each merchant's admin enters once in Scalekit's hosted form. Scalekit holds them in the token vault and handles authentication on every call, a virtual MCP server scopes tools by agent role so the dispute desk never sees payouts, and every execution returns an execution_id attributed to that merchant.

Build your PayPal agent with Scalekit

Browse the Scalekit PayPal connector and the PayPal connector docs with the full tool list. See Scalekit pricing, or talk to us.

No items found.
Agent
Auth Quickstart
On this page
Share this article
Scalekit
Agent attribution

Acquire enterprise customers with
‍zero upfront cost.

Every feature unlocked. No hidden fees.