Security review
Sample API callouts#
sample-api-callouts.pdf, the Sample API Callouts document uploaded to the AppExchange security review. Page and PDF are built from the same source.<angle brackets> are runtime substitutions; all other header values are exactly what the package sends.1. How every outbound request is built#
All outbound calls are issued by a single private method, StripeGateway.rawRequest — the only setEndpoint in the package — so the header set below is identical on every request in this document.
| Header | Value | Set by |
|---|---|---|
Authorization | Bearer rk_live_<restricted key> | The platform, from the Named Credential. Never set in packaged code and never visible to it. The key value does not appear in any packaged file, field, custom setting, static resource or log. |
Stripe-Version | 2026-03-25.dahlia | Package. Pinned in PaymentGatewayRegistry and upgraded only by a package upgrade, so a Stripe API change cannot alter behaviour in an installed org. |
Content-Type | application/x-www-form-urlencoded | Package. Stripe's API is form-encoded, not JSON. |
Idempotency-Key | <deterministic key> | Package, on write operations only. Omitted entirely when blank, so no empty header is sent. |
Endpoint resolution. The package sets the endpoint as callout:<NamedCredentialName> + <path>. The platform resolves that to https://api.stripe.com and injects the credential. Two credentials are used and are referred to below as:
DONOR KEY reachable on the public/guest path — write on Payment Intents, Setup Intents, Customers, Payment Methods; None on Charges, Refunds and Balance.
ADMIN KEY staff-only — adds write on Payment Intents, Charges, Refunds; read on Balance, Disputes, Customers, Payment Methods.
Request timeout: 30,000 ms on every call.
2. Outbound requests — payment capture#

Where the calls in this section happen. The two solid green arrows into Stripe are the server-side callouts documented below — POST /v1/payment_intents when the gift is staged and GET /v1/payment_intents/<id> when the outcome is read back. The red dashed arrows are Stripe.js traffic from the browser-hosted iframe and are not Apex callouts; they are shown so a reviewer can see that card capture, confirmation and 3-D Secure never pass through Salesforce. The purple arrows are the reconciler's re-fetch of any intent left open. No arrow runs from Stripe into the package: there is no webhook.
Creates the payment for a one-off gift. Called from StripeGateway.createPaymentIntent.
POST https://api.stripe.com/v1/payment_intents
Authorization: Bearer rk_live_<donor restricted key>
Stripe-Version: 2026-03-25.dahlia
Content-Type: application/x-www-form-urlencoded
Idempotency-Key: <staging record id>-intent
amount=5000¤cy=usd&automatic_payment_methods[enabled]=true
&metadata[correlation_id]=<uuid>&metadata[campaign_id]=<15-char Campaign Id>
&metadata[donor_email]=donor%40example.org
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "pi_3QxampleIntentId",
"object": "payment_intent",
"amount": 5000,
"currency": "usd",
"status": "requires_payment_method",
"client_secret": "pi_3QxampleIntentId_secret_<redacted>",
"metadata": { "correlation_id": "<uuid>", "campaign_id": "701...", "donor_email": "donor@example.org" }
}
Note. The client_secret is returned to the donor's browser so Stripe.js can confirm the payment. It is scoped to this one PaymentIntent and cannot be used to read or charge anything else.
Re-fetches the intent to establish the outcome. This call is why the package needs no webhook: payment status is pulled from Stripe rather than trusted from an inbound request.
GET https://api.stripe.com/v1/payment_intents/pi_3QxampleIntentId
Authorization: Bearer rk_live_<donor restricted key>
Stripe-Version: 2026-03-25.dahlia
Content-Type: application/x-www-form-urlencoded
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "pi_3QxampleIntentId",
"object": "payment_intent",
"amount": 5000,
"amount_received": 5000,
"currency": "usd",
"status": "succeeded",
"latest_charge": "ch_3QxampleChargeId"
}
Charges a stored payment method for a scheduled recurring gift, with no donor present. Called from the recurring-donation batch.
POST https://api.stripe.com/v1/payment_intents
Authorization: Bearer rk_live_<admin restricted key>
Stripe-Version: 2026-03-25.dahlia
Content-Type: application/x-www-form-urlencoded
Idempotency-Key: <recurring donation id>-<period>
amount=2500¤cy=usd&customer=cus_Example&payment_method=pm_Example
&off_session=true&confirm=true
HTTP/1.1 200 OK
Content-Type: application/json
{ "id": "pi_3QxampleRecurring", "status": "succeeded", "amount": 2500, "currency": "usd" }
Failure path. A card requiring authentication returns 402 with "code": "authentication_required". The package records the failure, does not retry immediately, and emails the donor a re-authentication link rather than raising an exception.
3. Outbound requests — stored payment methods#
Creates a Stripe Customer for a recurring or saved-card gift only — one-time gifts and event tickets are customerless. This is the call that transmits donor name and email to Stripe.
POST https://api.stripe.com/v1/customers
Authorization: Bearer rk_live_<donor restricted key>
Stripe-Version: 2026-03-25.dahlia
Content-Type: application/x-www-form-urlencoded
Idempotency-Key: <correlation id>-customer
email=donor%40example.org&name=Jane%20Donor
HTTP/1.1 200 OK
Content-Type: application/json
{ "id": "cus_Example", "object": "customer", "email": "donor@example.org", "name": "Jane Donor" }
Authorises a card for future off-session use without charging it.
POST https://api.stripe.com/v1/setup_intents
Authorization: Bearer rk_live_<donor restricted key>
Stripe-Version: 2026-03-25.dahlia
Content-Type: application/x-www-form-urlencoded
Idempotency-Key: <correlation id>-setup
customer=cus_Example&usage=off_session
HTTP/1.1 200 OK
{ "id": "seti_Example", "object": "setup_intent", "status": "requires_payment_method",
"client_secret": "seti_Example_secret_<redacted>" }
Attaches a payment method to a customer, sets it as the default, and reads back its non-sensitive metadata (brand, last four digits, expiry) for display on Payment_Method__c.
POST https://api.stripe.com/v1/payment_methods/pm_Example/attach
Authorization: Bearer rk_live_<donor restricted key>
Stripe-Version: 2026-03-25.dahlia
Content-Type: application/x-www-form-urlencoded
customer=cus_Example
GET https://api.stripe.com/v1/payment_methods/pm_Example
Authorization: Bearer rk_live_<donor restricted key>
Stripe-Version: 2026-03-25.dahlia
Content-Type: application/x-www-form-urlencoded
HTTP/1.1 200 OK
{ "id": "pm_Example", "type": "card",
"card": { "brand": "visa", "last4": "4242", "exp_month": 12, "exp_year": 2029 } }
Note. last4, brand and expiry are the only card attributes the package ever receives or stores. No full card number is returned by this endpoint.
4. Outbound requests — back office#
Refunds a gift. Initiated by a staff user holding the Process_Refunds custom permission, from the Opportunity record page. The donor credential cannot perform this call — its Refunds permission is None.
POST https://api.stripe.com/v1/refunds
Authorization: Bearer rk_live_<admin restricted key>
Stripe-Version: 2026-03-25.dahlia
Content-Type: application/x-www-form-urlencoded
Idempotency-Key: <opportunity id>-refund
payment_intent=pi_3QxampleIntentId&amount=5000&reason=requested_by_customer
HTTP/1.1 200 OK
{ "id": "re_Example", "object": "refund", "amount": 5000, "currency": "usd",
"payment_intent": "pi_3QxampleIntentId", "status": "succeeded" }
Scheduled jobs list recent activity to reconcile against org records and to detect payments that completed while a browser session was interrupted.
GET https://api.stripe.com/v1/payment_intents?limit=100&created%5Bgte%5D=1789000000
Authorization: Bearer rk_live_<admin restricted key>
Stripe-Version: 2026-03-25.dahlia
Content-Type: application/x-www-form-urlencoded
HTTP/1.1 200 OK
{ "object": "list", "has_more": false, "data": [
{ "id": "pi_3QxampleIntentId", "status": "succeeded", "amount": 5000,
"metadata": { "correlation_id": "<uuid>" } } ] }
The /v1/refunds and /v1/disputes list calls take the same shape and the same headers, differing only in path.
5. Outbound requests — setup verification#
These calls exist only so an administrator can prove, at configuration time, that the credentials were built with the intended scopes. They are issued from the Settings console, never from a guest path.
Confirms the administrator credential works. /v1/account is deliberately not used, so no key is ever required to hold Accounts: Read.
GET https://api.stripe.com/v1/refunds?limit=1
Authorization: Bearer rk_live_<admin restricted key>
Stripe-Version: 2026-03-25.dahlia
Content-Type: application/x-www-form-urlencoded
HTTP/1.1 200 OK
{ "object": "list", "data": [], "has_more": false }
A deliberate, empty POST whose expected outcome is a permission error. On this endpoint Stripe checks permission before validating parameters, so a correctly scoped donor key is refused and nothing is created. Charges and Refunds share one permission, so the refusal is proof the guest-held key cannot refund.
POST https://api.stripe.com/v1/charges Authorization: Bearer rk_live_<donor restricted key> Stripe-Version: 2026-03-25.dahlia Content-Type: application/x-www-form-urlencoded (empty body) HTTP/1.1 403 Forbidden <-- PASS: the key is correctly scoped { "error": { "type": "invalid_request_error", "message": "The provided key does not have the required permissions ... charge_write" } } HTTP/1.1 400 Bad Request <-- FAIL: the key is over-scoped; setup step reports an error { "error": { "code": "parameter_missing", "message": "Must provide source or customer." } }
Why this endpoint and no other. It is the only Stripe endpoint with both required properties: permission is checked before validation, and an empty POST creates nothing. An empty POST to /v1/customers or /v1/setup_intents succeeds and leaves a record behind; /v1/refunds validates before checking permission and so answers 400 to a correctly scoped and an over-scoped key alike. Both alternatives were measured in a Stripe sandbox during the scope audit of 7 September 2026 and rejected on the evidence. No charge is created in either outcome.
Reads an endpoint deliberately outside everything the administrator key is asked to hold, confirming the key is not over-scoped. A correctly scoped key returns 403.
GET https://api.stripe.com/v1/products?limit=1
Authorization: Bearer rk_live_<admin restricted key>
Stripe-Version: 2026-03-25.dahlia
Content-Type: application/x-www-form-urlencoded
HTTP/1.1 403 Forbidden <-- PASS: the key holds nothing beyond its audited scopes
6. Inbound — the one endpoint this package exposes#
EmbedConfigResource — the only global class in the package (1 of 293). Served by the subscriber's own Salesforce Site. Returns campaign display configuration to the embeddable donation widget hosted on a third-party website.
GET https://<subscriber-site>/services/apexrest/pledgivo/embed/config?campaignId=<id>
Origin: https://www.example-charity.org
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://www.example-charity.org
{ "success": true,
"data": { "campaignName": "Winter Appeal", "goalAmount": 25000, "currency": "USD",
"suggestedAmounts": [25, 50, 100], "themeTokens": { "--pf-brand-primary": "#C4462F" } } }
HTTP/1.1 200 OK <-- origin not on the subscriber's allowlist
{ "success": false, "error": "This request could not be completed." }
Security properties. GET only, no write path. Returns no donor records and no personal data of any kind. Answers only origins on the subscriber's own allowlist in Settings__c. Requires no authentication by design — it serves public campaign display data to a public web page — and it sanitizes its own error text by hand rather than relying on the shared ControllerAction wrapper, because it is read by third-party JavaScript. Theme tokens are validated against a protected custom metadata catalogue before being emitted.
7. Endpoints this package does not call#
- No inbound webhook of any kind. The package registers no webhook with Stripe, exposes no webhook receiver, and therefore stores no webhook signing secret. Payment outcomes are always pulled.
- No
/v1/account. Removed deliberately on 7 September 2026 so that no credential is ever required to hold Accounts: Read. The Payments settings panel prints no Stripe account name as a direct consequence. - No
http://endpoint anywhere. Every outbound call is TLS. - No publisher-operated endpoint in the data path. CloudAlgo hosts no server, API, database or logging service that the package calls, and the package emits no telemetry. The publisher's subscription checkout (
billing.cloudalgo.com) and documentation site (pledgivo.cloudalgo.com) are reached only by a staff user clicking a hyperlink that opens in a new tab; the package issues no request to either host and registers no CSP trusted site for them. - No Salesforce licence-manager callout. Seat checks use the platform's local
UserInfo.isCurrentUserLicensed()and aPackageLicensequery; nothing leaves the org.