Partner app installs
Let merchants connect your product to Flint through a hosted install flow.
At a high level:
- create a partner app
- send a merchant to Flint's hosted authorize URL
- receive an authorization code at your redirect URI
- exchange the code, with your PKCE verifier, for a partner install access token
- call normal Flint
/v1/...APIs with that token - optionally subscribe to partner app webhooks
If you still need credentials for your first Flint merchant, use API & agent onboarding to provision the merchant and mint its first key, or create both in the dashboard.
Before you start#
- A Flint merchant account you control
- Either an
onboarding_session_tokenor a live external API key withdeveloper.partner_apps.write - At least one redirect URI you control
- A backend that can safely store
client_secret, refresh tokens, and webhook secrets
Treat state as an opaque CSRF and request-correlation value. Generate it per install attempt and validate it on the callback.
Step 1: create the partner app#
curl -X POST https://api.withflintpay.com/v1/developer/partner/apps \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY_OR_DEVELOPER_SESSION" \
-d '{
"name": "Acme OMS",
"app_type": "server",
"visibility": "private",
"redirect_uris": [
"https://acme.example.com/flint/oauth/callback",
"http://localhost:3000/flint/oauth/callback"
],
"permission_manifest": [
{
"permission_id": "manage_orders",
"title": "Manage orders",
"description": "Read and update Flint orders for installed merchants.",
"required_scopes": [
"commerce.orders.read",
"commerce.orders.write"
]
},
{
"permission_id": "read_customers",
"title": "Read customers",
"description": "Read customer records for installed merchants.",
"required_scopes": [
"customers.read"
],
"optional": true
}
],
"default_requested_permissions": [
"manage_orders"
]
}'
Store:
data.partner_app_iddata.client_iddata.client_secret
client_secret is shown once. Flint cannot return the same value later.
Step 2: build the hosted install URL#
Generate a PKCE pair for each install attempt, then send the merchant to:
https://api.withflintpay.com/v1/oauth/authorize
with these query parameters:
response_type=codeclient_idredirect_urimode=test|livestatecode_challengecode_challenge_method=S256- optional
scope, a space-separated list of permission IDs - optional
sandbox_id, withmode=testonly
Generate a PKCE pair#
Flint requires S256 PKCE (RFC 7636) on every authorization. Create a code_verifier of 43 to 128 URL-safe characters, store it with the state for this attempt, and send code_challenge, the unpadded base64url SHA-256 hash of the verifier:
import { createHash, randomBytes } from "node:crypto";
const codeVerifier = randomBytes(32).toString("base64url");
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
// Store codeVerifier with state. Send codeChallenge as code_challenge.
code_challenge must be exactly 43 characters. plain isn't supported.
Example:
https://api.withflintpay.com/v1/oauth/authorize?response_type=code&client_id=fpc_0123456789abcdef&redirect_uri=https%3A%2F%2Facme.example.com%2Fflint%2Foauth%2Fcallback&mode=test&state=merchant-session-42&scope=manage_orders%20read_customers&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256
Rules to remember:
redirect_urimust exactly match a registered redirect URI, and registered redirect URIs can't contain a fragmentscopenames permission IDs from your permission manifest, separated by spaces. Commas aren't separators- if
scopeis omitted, Flint usesdefault_requested_permissions - all non-optional permissions are always granted
- Flint ignores parameters it doesn't recognize, such as the removed
permission_ids - each parameter may appear once
- with
mode=test,sandbox_idnames the sandbox to install into; if it is omitted, Flint uses the merchant's default sandbox - with
mode=live, sendingsandbox_idreturnsinvalid_request - an install has one test grant, and it points at one sandbox. Authorizing
mode=testagain moves the install's test access to the sandbox you name, or to the merchant's current default sandbox when you omitsandbox_id, and revokes the old test tokens. Sendsandbox_idto keep the same sandbox - live installs require the merchant to have completed onboarding
Flint shows the merchant a consent screen. They sign in or create an account, choose the merchant to install on, review the permissions and the access each one grants, and choose Install app or Deny. Test installs go to sandbox_id, or to the merchant's default sandbox when you omit it.
Optional: preview before redirecting#
If you want to validate the request or show a preflight summary in your own product, call:
curl "https://api.withflintpay.com/v1/oauth/authorize/preview?response_type=code&client_id=FPC_CLIENT_ID&redirect_uri=https%3A%2F%2Facme.example.com%2Fflint%2Foauth%2Fcallback&mode=test&state=merchant-session-42&scope=manage_orders%20read_customers"
The preview takes the same query parameters as /v1/oauth/authorize, including sandbox_id with mode=test. code_challenge and code_challenge_method are optional on the preview, and Flint validates them when you send them. Sending sandbox_id with mode=live returns 400 INVALID_REQUEST with param sandbox_id. The preview doesn't know the merchant yet, so it doesn't check that the sandbox exists; the consent screen does. The response returns the app metadata plus the exact permission set Flint will request from the merchant. Each entry in requested_permissions carries required_scopes, the Flint API scopes that permission grants. Flint sets them; the title and description come from your permission manifest.
Step 3: handle the callback#
When the merchant chooses Install app, Flint redirects back to your redirect_uri with:
codestatemode
Example:
https://acme.example.com/flint/oauth/callback?code=fpac_abc123&state=merchant-session-42&mode=test
On a redirectable failure, Flint sends:
errorerror_descriptionstate
Example:
https://acme.example.com/flint/oauth/callback?error=access_denied&error_description=The+merchant+declined+the+install.&state=merchant-session-42
error is one of the standard OAuth values:
error | Cause |
|---|---|
unsupported_response_type | response_type is not code. |
invalid_request | state is missing, mode is not test or live, sandbox_id is sent with mode=live, code_challenge is missing or isn't S256, or a parameter is repeated. |
invalid_scope | scope names a permission that isn't in your app's permission manifest, or is empty. |
access_denied | The merchant chose Deny. |
server_error | Flint couldn't complete the request. Retry the authorization later. |
error_description is fixed text for people, not for parsing. Flint never redirects when client_id is unknown or redirect_uri isn't registered; the merchant sees the error on Flint instead. Problems the merchant can fix also stay on Flint: signing in, identity verification, finishing onboarding for a live install, admin access, and a sandbox_id that belongs to another merchant. The merchant can resolve them or choose Deny.
On the callback:
- validate that
statematches the value you issued - reject the callback if
erroris present - store
modealongside the install - exchange
codeimmediately on your backend
Step 4: exchange the authorization code#
Use your app's client_id and client_secret, and the code_verifier you stored with this attempt's state.
curl -X POST https://api.withflintpay.com/v1/oauth/token \
-u "FPC_CLIENT_ID:FPS_CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=FPAC_AUTHORIZATION_CODE" \
-d "redirect_uri=https://acme.example.com/flint/oauth/callback" \
-d "code_verifier=YOUR_CODE_VERIFIER"
The token endpoint accepts only application/x-www-form-urlencoded bodies, with no URL query string. Authenticate with HTTP Basic, as above, or with client_id and client_secret form fields, but not both together. Flint ignores form fields it doesn't recognize. Each field may appear once.
A wrong code_verifier returns invalid_grant and consumes the code, so the merchant has to authorize again. Reusing a code that Flint already exchanged also returns invalid_grant and revokes the tokens that code issued.
Example response:
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "fprt_abc123",
"scope": "manage_orders read_customers",
"merchant_id": "mer_123",
"partner_app_id": "papp_123",
"partner_app_install_id": "pinst_123",
"environment_grant_id": "egrt_123",
"mode": "test",
"sandbox_id": "test_01KWJS7ZW5YAC86HMHWTCV93GX"
}
scope lists the permission IDs the merchant granted, sorted and separated by spaces, including required permissions you didn't list. It isn't a list of API scopes. To see the API scopes behind each permission, read requested_permissions[].required_scopes from the preview, or call GET /v1/developer/auth-context with the access token. sandbox_id is present exactly when mode is test.
Store:
refresh_tokenmerchant_idpartner_app_install_idenvironment_grant_idmodesandbox_id, for test mode- granted
scope, the permission IDs
Credentials last for these periods:
| Credential | Lifetime |
|---|---|
| Authorization code | 5 minutes, single use |
| Access token | 1 hour |
| Refresh token | Single use. Expires 30 days after Flint issues it, so a refresh token you don't use for 30 days expires. |
Step 5: call Flint APIs with the install token#
Use the returned access token on the normal Flint public API.
curl https://api.withflintpay.com/v1/orders \
-H "Authorization: Bearer PARTNER_INSTALL_ACCESS_TOKEN"
The partner install token only works for scopes granted during install.
Examples:
- if the install grants
commerce.orders.read, you can read orders - if the install grants
payments.payment_intents.read, you can read payment intents - if the install does not grant a route's required scope, Flint returns
403 INSUFFICIENT_SCOPE
The /v1/onboarding/... routes and POST /v1/merchant-account-sessions reject partner install tokens with 401 INVALID_CREDENTIAL and details[0].reason wrong_credential_type. A partner install with merchants.profile.read can read the Merchant resource, including its readiness: payments, payouts, and every capability. Onboarding and verification remediation still run under the merchant's own API key.
Do not send a partner install token and an external API key on the same request.
Step 6: refresh tokens#
When the access token expires, rotate the refresh token:
curl -X POST https://api.withflintpay.com/v1/oauth/token \
-u "FPC_CLIENT_ID:FPS_CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=FPRT_REFRESH_TOKEN"
Replace the stored refresh token with the new one from the response. Each refresh token works once. A refresh token you don't use for 30 days expires, so refresh at least monthly, for example from a daily job.
You can send scope on a refresh, but it must list exactly the granted permission IDs. Any other value returns invalid_scope. Omit it.
A refresh fails with 400 invalid_grant when the token is unknown, expired, or no longer valid because the merchant revoked the install or environment grant, or because a newer authorization replaced it. Send the merchant through authorization again.
If a refresh response is lost, retry with the same refresh token within 60 seconds. Flint issues a new pair and invalidates the one you never received. After 60 seconds, or once you've used the new refresh token, reusing an old one is treated as a leak. Flint revokes that environment grant's tokens and returns invalid_grant. The other mode is unaffected, and the install and grant stay active. Send the merchant through authorization again for that mode. Refresh from one worker at a time: two workers refreshing with the same token end the token family.
If you refresh too often, Flint returns 429 with a Retry-After header. Wait that many seconds before retrying.
Revoke tokens#
Revoke tokens you no longer need with RFC 7009 revocation:
curl -X POST https://api.withflintpay.com/v1/oauth/revoke \
-u "FPC_CLIENT_ID:FPS_CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=FPRT_REFRESH_TOKEN" \
-d "token_type_hint=refresh_token"
Authenticate the same way as the token endpoint. token can be a refresh token or an access token, and token_type_hint (refresh_token or access_token) only helps Flint find it.
- Revoking a refresh token ends its token family: the refresh token and the access tokens issued from it.
- Revoking an access token invalidates every outstanding access token for its environment grant. The refresh token keeps working.
- Flint returns
200with an empty body for unknown, expired, and other apps' tokens, and does nothing.
Revoking tokens doesn't uninstall your app or change the install or grant. To disconnect a merchant, use the install and environment grant revoke routes under Inspect installs later.
Discovery#
Libraries that configure themselves from server metadata can read https://api.withflintpay.com/.well-known/oauth-authorization-server (RFC 8414).
Step 7: subscribe to partner webhooks#
A partner app can have several webhook endpoints, and one endpoint can carry both kinds of partner events:
- app lifecycle events (
event_sourcesincludespartner_app), to track installs and revocations - installed-merchant events (
event_sourcesincludesinstalled_merchants), to receive Flint resource events for merchants that installed your app
Partner endpoints are created on POST /v1/webhook-endpoints, the same route that registers a merchant's own endpoints. Naming partner_app_id and a partner event_sources value is what makes the endpoint a partner endpoint.
Use the app owner's API key with webhooks.write to create or change endpoints, rotate signing secrets, send test events, or resend deliveries. Install tokens cannot perform these operations and return 401 INVALID_CREDENTIAL with reason wrong_credential_type.
App lifecycle events#
curl -X POST https://api.withflintpay.com/v1/webhook-endpoints \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"url": "https://acme.example.com/flint/partner-webhooks",
"event_sources": ["partner_app"],
"partner_app_id": "PAPP_ID",
"enabled_events": ["*"],
"status": "active"
}'
enabled_events is required. For event_sources: ["partner_app"], ["*"] subscribes you to all lifecycle events, including any Flint adds later. You can also list the ones you want. The lifecycle events are:
partner_app_install.createdpartner_app_install.updatedpartner_app_install.permissions_updatedpartner_app_install.revokedenvironment_grant.createdenvironment_grant.revoked
Lifecycle events belong to your app, not to one merchant's test or live data, so Flint delivers them in both modes.
Installed-merchant events#
curl -X POST https://api.withflintpay.com/v1/webhook-endpoints \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"url": "https://acme.example.com/flint/merchant-events",
"event_sources": ["installed_merchants"],
"partner_app_id": "PAPP_ID",
"installed_merchant_modes": ["test", "live"],
"enabled_events": [
"order.paid",
"payment_intent.succeeded",
"refund.updated"
],
"status": "active"
}'
When event_sources includes installed_merchants:
enabled_eventsrequires at least one installed-merchant event, listed by name.["*"]returns400 INVALID_REQUESTwithdetails[0].codeNOT_ALLOWED, because these events depend on the permissions each install grantsinstalled_merchant_modesis required:["test"],["live"], or["test", "live"]. Flint forwards installed-merchant events from those modes only. Withoutinstalled_merchants, sendinginstalled_merchant_modesreturns400 INVALID_REQUESTwithdetails[0].codeNOT_ALLOWEDpartner_app_idis required, and names the app whose installs are forwarded- Flint only forwards an event if the install has the read scope required for that event type
To receive both kinds on one endpoint, send "event_sources": ["partner_app", "installed_merchants"] and list the lifecycle and installed-merchant events you want in enabled_events. Lifecycle events still arrive in both modes; installed_merchant_modes limits only the installed-merchant events.
Read the endpoints back with GET /v1/webhook-endpoints?event_sources=installed_merchants, and read the events themselves with GET /v1/webhook-events?event_source=installed_merchants. The delivered event carries event_source singular; the endpoint configures event_sources plural.
The complete eligibility table below is generated from Flint's delivery authorization policy. A matching write scope satisfies the listed read scope.
| Installed-merchant event | Required partner permission |
|---|---|
bundle.archived | commerce.bundles.read |
bundle.created | commerce.bundles.read |
bundle.updated | commerce.bundles.read |
category.created | commerce.catalog.read |
category.deleted | commerce.catalog.read |
category.updated | commerce.catalog.read |
checkout_session.closed | checkouts.checkout_sessions.read |
checkout_session.completed | checkouts.checkout_sessions.read |
checkout_session.expired | checkouts.checkout_sessions.read |
checkout_session.invalidated | checkouts.checkout_sessions.read |
credit_note.created | commerce.credit_notes.read |
credit_note.issued | commerce.credit_notes.read |
credit_note.updated | commerce.credit_notes.read |
credit_note.voided | commerce.credit_notes.read |
credit_note_allocation.created | commerce.credit_notes.read |
credit_note_allocation.reversed | commerce.credit_notes.read |
customer.created | customers.read |
customer.updated | customers.read |
customer_deletion_request.completed | customers.read |
customer_deletion_request.created | customers.read |
customer_deletion_request.rejected | customers.read |
delivery_location_set.activated | commerce.delivery.read |
delivery_location_set.archived | commerce.delivery.read |
delivery_location_set.created | commerce.delivery.read |
delivery_location_set.deactivated | commerce.delivery.read |
delivery_location_set.updated | commerce.delivery.read |
delivery_method.activated | commerce.delivery.read |
delivery_method.archived | commerce.delivery.read |
delivery_method.created | commerce.delivery.read |
delivery_method.deactivated | commerce.delivery.read |
delivery_method.updated | commerce.delivery.read |
delivery_profile.activated | commerce.delivery.read |
delivery_profile.archived | commerce.delivery.read |
delivery_profile.created | commerce.delivery.read |
delivery_profile.deactivated | commerce.delivery.read |
delivery_profile.updated | commerce.delivery.read |
delivery_rate_callback.activated | commerce.delivery.read |
delivery_rate_callback.archived | commerce.delivery.read |
delivery_rate_callback.created | commerce.delivery.read |
delivery_rate_callback.deactivated | commerce.delivery.read |
delivery_rate_callback.updated | commerce.delivery.read |
delivery_revocation.created | commerce.delivery.read |
delivery_selection.committed | commerce.delivery.read |
delivery_zone.activated | commerce.delivery.read |
delivery_zone.archived | commerce.delivery.read |
delivery_zone.created | commerce.delivery.read |
delivery_zone.deactivated | commerce.delivery.read |
delivery_zone.updated | commerce.delivery.read |
fraud_warning.created | risk.read |
fraud_warning.updated | risk.read |
fulfillment.completed | commerce.orders.read |
fulfillment.created | commerce.orders.read |
fulfillment.status_changed | commerce.orders.read |
fulfillment.updated | commerce.orders.read |
fulfillment_event.created | commerce.orders.read |
gift_card.created | commerce.gift_cards.read |
gift_card.updated | commerce.gift_cards.read |
gift_card_load.created | commerce.gift_cards.read |
gift_card_load.updated | commerce.gift_cards.read |
gift_card_notification.created | commerce.gift_cards.read |
gift_card_notification.updated | commerce.gift_cards.read |
gift_card_redemption.created | commerce.gift_cards.read |
gift_card_redemption.updated | commerce.gift_cards.read |
gift_card_transaction.created | commerce.gift_cards.read |
inventory_count.applied | commerce.inventory.read |
inventory_level.shortage_detected | commerce.inventory.read |
inventory_level.updated | commerce.inventory.read |
inventory_receipt.created | commerce.inventory.read |
inventory_reservation.action_required | commerce.inventory.read |
inventory_reservation.at_risk | commerce.inventory.read |
inventory_reservation.closed | commerce.inventory.read |
inventory_reservation.committed | commerce.inventory.read |
inventory_reservation.consumed | commerce.inventory.read |
inventory_reservation.created | commerce.inventory.read |
inventory_reservation.hold_expired | commerce.inventory.read |
inventory_reservation.released | commerce.inventory.read |
inventory_transfer.closed | commerce.inventory.read |
inventory_transfer.departed | commerce.inventory.read |
inventory_transfer.lost | commerce.inventory.read |
inventory_transfer.received | commerce.inventory.read |
inventory_transfer.returned | commerce.inventory.read |
invoice.collection_block_resolved | commerce.invoices.read |
invoice.collection_blocked | commerce.invoices.read |
invoice.created | commerce.invoices.read |
invoice.credited | commerce.invoices.read |
invoice.delivery_failed | commerce.invoices.read |
invoice.delivery_succeeded | commerce.invoices.read |
invoice.issue_failed | commerce.invoices.read |
invoice.issued | commerce.invoices.read |
invoice.late_fee_assessed | commerce.invoices.read |
invoice.late_fee_due | commerce.invoices.read |
invoice.late_fee_waived | commerce.invoices.read |
invoice.manual_payment_recorded | commerce.invoices.read |
invoice.manual_payment_reversed | commerce.invoices.read |
invoice.marked_uncollectible | commerce.invoices.read |
invoice.overdue | commerce.invoices.read |
invoice.paid | commerce.invoices.read |
invoice.partially_paid | commerce.invoices.read |
invoice.partially_refunded | commerce.invoices.read |
invoice.refunded | commerce.invoices.read |
invoice.reminder_due | commerce.invoices.read |
invoice.reopened | commerce.invoices.read |
invoice.sent | commerce.invoices.read |
invoice.updated | commerce.invoices.read |
invoice.voided | commerce.invoices.read |
invoice_payment_attempt.canceled | commerce.invoices.read |
invoice_payment_attempt.expired | commerce.invoices.read |
invoice_payment_attempt.failed | commerce.invoices.read |
invoice_payment_attempt.processing | commerce.invoices.read |
invoice_payment_attempt.settled | commerce.invoices.read |
modifier_group.archived | commerce.catalog.read |
modifier_group.created | commerce.catalog.read |
modifier_group.updated | commerce.catalog.read |
modifier_set.archived | commerce.catalog.read |
modifier_set.created | commerce.catalog.read |
modifier_set.updated | commerce.catalog.read |
order.closed | commerce.orders.read |
order.created | commerce.orders.read |
order.inventory_exception_created | commerce.orders.read |
order.inventory_exception_resolved | commerce.orders.read |
order.paid | commerce.orders.read |
order.partially_paid | commerce.orders.read |
order.payment_authorization_canceled | commerce.orders.read |
order.payment_authorization_expired | commerce.orders.read |
order.payment_authorized | commerce.orders.read |
order.payment_captured | commerce.orders.read |
order.refunded | commerce.orders.read |
order.updated | commerce.orders.read |
package.created | commerce.orders.read |
package.updated | commerce.orders.read |
payment_intent.canceled | payments.payment_intents.read |
payment_intent.fulfillment_hold_updated | payments.payment_intents.read |
payment_intent.payment_failed | payments.payment_intents.read |
payment_intent.processing | payments.payment_intents.read |
payment_intent.requires_action | payments.payment_intents.read |
payment_intent.requires_capture | payments.payment_intents.read |
payment_intent.succeeded | payments.payment_intents.read |
payment_link.created | checkouts.payment_links.read |
payment_link.updated | checkouts.payment_links.read |
payment_method.failed | payments.payment_methods.read |
payment_method.removed | payments.payment_methods.read |
payment_method.saved | payments.payment_methods.read |
payment_review.closed | risk.read |
payment_review.opened | risk.read |
payment_review.reopened | risk.read |
product.archived | commerce.products.read |
product.created | commerce.products.read |
product.updated | commerce.products.read |
product_variant.archived | commerce.products.read |
product_variant.created | commerce.products.read |
product_variant.updated | commerce.products.read |
promotion.archived | commerce.promotions.read |
promotion.created | commerce.promotions.read |
promotion.updated | commerce.promotions.read |
promotion_code.archived | commerce.promotions.read |
promotion_code.created | commerce.promotions.read |
promotion_code.updated | commerce.promotions.read |
refund.created | commerce.refunds.read |
refund.failed | commerce.refunds.read |
refund.updated | commerce.refunds.read |
report.failed | reports.read |
report.succeeded | reports.read |
return.canceled | commerce.returns.read |
return.completed | commerce.returns.read |
return.created | commerce.returns.read |
return.decision_recorded | commerce.returns.read |
return.reopened | commerce.returns.read |
return.updated | commerce.returns.read |
return_disposition.created | commerce.returns.read |
return_disposition.updated | commerce.returns.read |
return_inspection.acceptance_decided | commerce.returns.read |
return_inspection.created | commerce.returns.read |
return_inspection.superseded | commerce.returns.read |
return_receipt.created | commerce.returns.read |
return_receipt.superseded | commerce.returns.read |
return_receipt.verified | commerce.returns.read |
return_resolution.created | commerce.returns.read |
return_resolution.updated | commerce.returns.read |
shipment.created | commerce.orders.read |
shipment.updated | commerce.orders.read |
subscription.activated | commerce.subscriptions.read |
subscription.canceled | commerce.subscriptions.read |
subscription.cancellation_scheduled | commerce.subscriptions.read |
subscription.created | commerce.subscriptions.read |
subscription.cycle_skipped | commerce.subscriptions.read |
subscription.delivery_action_required | commerce.subscriptions.read |
subscription.delivery_pause_upcoming | commerce.subscriptions.read |
subscription.delivery_updated | commerce.subscriptions.read |
subscription.dunning_exhausted | commerce.subscriptions.read |
subscription.past_due | commerce.subscriptions.read |
subscription.paused | commerce.subscriptions.read |
subscription.payment_failed | commerce.subscriptions.read |
subscription.payment_succeeded | commerce.subscriptions.read |
subscription.reactivated | commerce.subscriptions.read |
subscription.renewal_upcoming | commerce.subscriptions.read |
subscription.resumed | commerce.subscriptions.read |
subscription.trial_ending | commerce.subscriptions.read |
subscription.updated | commerce.subscriptions.read |
subscription_delivery_migration.completed | commerce.subscriptions.read |
subscription_payment_retry.created | commerce.subscriptions.read |
subscription_payment_retry.failed | commerce.subscriptions.read |
subscription_payment_retry.succeeded | commerce.subscriptions.read |
Inspect installs later#
App owners use these routes from their merchant auth context:
GET /v1/developer/partner/appsGET /v1/developer/partner/apps/{partner_app_id}GET /v1/developer/partner/apps/{partner_app_id}/installsGET /v1/developer/partner/apps/{partner_app_id}/installs/{partner_app_install_id}
Installing merchants use GET /v1/developer/partner/installs with developer.partner_apps.read to discover their own installs and environment grants across all apps. They can inspect a returned install with GET /v1/developer/partner/apps/{partner_app_id}/installs/{partner_app_install_id}. App owners keep access to all installs of their own apps.
The installing merchant or app owner can use these revocation routes with a live external API key with developer.partner_apps.write when the merchant disconnects:
POST /v1/developer/partner/apps/{partner_app_id}/installs/{partner_app_install_id}/revokePOST /v1/developer/partner/apps/{partner_app_id}/installs/{partner_app_install_id}/environment-grants/{environment_grant_id}/revoke
POST /v1/developer/partner/apps/{partner_app_id}/rotate-secret also requires a live external API key with developer.partner_apps.write. These three routes do not accept an onboarding_session_token.
Production checklist#
- store
client_secret, refresh tokens, and webhook secrets securely - generate a unique
statevalue per install attempt - validate
stateon every callback - exchange authorization codes only on your backend
- keep each
code_verifierwith itsstateand send it on exchange - record
merchant_id,partner_app_install_id,environment_grant_id, andmode - use webhook deliveries as the durable signal for install lifecycle changes
- refresh tokens server-side and rotate stored refresh tokens atomically
- test both
testandliveinstalls explicitly
Next steps#
- Partner Apps API reference for the full route surface
- Webhooks guide for signature verification patterns
- Rate limits for retry and worker planning
