Skip to content

Partner app installs

Let merchants connect your product to Flint through a hosted install flow.

At a high level:

  1. create a partner app
  2. send a merchant to Flint's hosted authorize URL
  3. receive an authorization code at your redirect URI
  4. exchange the code, with your PKCE verifier, for a partner install access token
  5. call normal Flint /v1/... APIs with that token
  6. optionally subscribe to partner app webhooks
Note:

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_token or a live external API key with developer.partner_apps.write
  • At least one redirect URI you control
  • A backend that can safely store client_secret, refresh tokens, and webhook secrets
Warning:

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
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_id
  • data.client_id
  • data.client_secret
Warning:

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:

Text
https://api.withflintpay.com/v1/oauth/authorize

with these query parameters:

  • response_type=code
  • client_id
  • redirect_uri
  • mode=test|live
  • state
  • code_challenge
  • code_challenge_method=S256
  • optional scope, a space-separated list of permission IDs
  • optional sandbox_id, with mode=test only

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:

JavaScript
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:

Text
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_uri must exactly match a registered redirect URI, and registered redirect URIs can't contain a fragment
  • scope names permission IDs from your permission manifest, separated by spaces. Commas aren't separators
  • if scope is omitted, Flint uses default_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_id names the sandbox to install into; if it is omitted, Flint uses the merchant's default sandbox
  • with mode=live, sending sandbox_id returns invalid_request
  • an install has one test grant, and it points at one sandbox. Authorizing mode=test again moves the install's test access to the sandbox you name, or to the merchant's current default sandbox when you omit sandbox_id, and revokes the old test tokens. Send sandbox_id to 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
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:

  • code
  • state
  • mode

Example:

Text
https://acme.example.com/flint/oauth/callback?code=fpac_abc123&state=merchant-session-42&mode=test

On a redirectable failure, Flint sends:

  • error
  • error_description
  • state

Example:

Text
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:

errorCause
unsupported_response_typeresponse_type is not code.
invalid_requeststate 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_scopescope names a permission that isn't in your app's permission manifest, or is empty.
access_deniedThe merchant chose Deny.
server_errorFlint 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:

  1. validate that state matches the value you issued
  2. reject the callback if error is present
  3. store mode alongside the install
  4. exchange code immediately 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
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:

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_token
  • merchant_id
  • partner_app_install_id
  • environment_grant_id
  • mode
  • sandbox_id, for test mode
  • granted scope, the permission IDs

Credentials last for these periods:

CredentialLifetime
Authorization code5 minutes, single use
Access token1 hour
Refresh tokenSingle 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
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.

Warning:

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
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
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 200 with 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_sources includes partner_app), to track installs and revocations
  • installed-merchant events (event_sources includes installed_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
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.created
  • partner_app_install.updated
  • partner_app_install.permissions_updated
  • partner_app_install.revoked
  • environment_grant.created
  • environment_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
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_events requires at least one installed-merchant event, listed by name. ["*"] returns 400 INVALID_REQUEST with details[0].code NOT_ALLOWED, because these events depend on the permissions each install grants
  • installed_merchant_modes is required: ["test"], ["live"], or ["test", "live"]. Flint forwards installed-merchant events from those modes only. Without installed_merchants, sending installed_merchant_modes returns 400 INVALID_REQUEST with details[0].code NOT_ALLOWED
  • partner_app_id is 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 eventRequired partner permission
bundle.archivedcommerce.bundles.read
bundle.createdcommerce.bundles.read
bundle.updatedcommerce.bundles.read
category.createdcommerce.catalog.read
category.deletedcommerce.catalog.read
category.updatedcommerce.catalog.read
checkout_session.closedcheckouts.checkout_sessions.read
checkout_session.completedcheckouts.checkout_sessions.read
checkout_session.expiredcheckouts.checkout_sessions.read
checkout_session.invalidatedcheckouts.checkout_sessions.read
credit_note.createdcommerce.credit_notes.read
credit_note.issuedcommerce.credit_notes.read
credit_note.updatedcommerce.credit_notes.read
credit_note.voidedcommerce.credit_notes.read
credit_note_allocation.createdcommerce.credit_notes.read
credit_note_allocation.reversedcommerce.credit_notes.read
customer.createdcustomers.read
customer.updatedcustomers.read
customer_deletion_request.completedcustomers.read
customer_deletion_request.createdcustomers.read
customer_deletion_request.rejectedcustomers.read
delivery_location_set.activatedcommerce.delivery.read
delivery_location_set.archivedcommerce.delivery.read
delivery_location_set.createdcommerce.delivery.read
delivery_location_set.deactivatedcommerce.delivery.read
delivery_location_set.updatedcommerce.delivery.read
delivery_method.activatedcommerce.delivery.read
delivery_method.archivedcommerce.delivery.read
delivery_method.createdcommerce.delivery.read
delivery_method.deactivatedcommerce.delivery.read
delivery_method.updatedcommerce.delivery.read
delivery_profile.activatedcommerce.delivery.read
delivery_profile.archivedcommerce.delivery.read
delivery_profile.createdcommerce.delivery.read
delivery_profile.deactivatedcommerce.delivery.read
delivery_profile.updatedcommerce.delivery.read
delivery_rate_callback.activatedcommerce.delivery.read
delivery_rate_callback.archivedcommerce.delivery.read
delivery_rate_callback.createdcommerce.delivery.read
delivery_rate_callback.deactivatedcommerce.delivery.read
delivery_rate_callback.updatedcommerce.delivery.read
delivery_revocation.createdcommerce.delivery.read
delivery_selection.committedcommerce.delivery.read
delivery_zone.activatedcommerce.delivery.read
delivery_zone.archivedcommerce.delivery.read
delivery_zone.createdcommerce.delivery.read
delivery_zone.deactivatedcommerce.delivery.read
delivery_zone.updatedcommerce.delivery.read
fraud_warning.createdrisk.read
fraud_warning.updatedrisk.read
fulfillment.completedcommerce.orders.read
fulfillment.createdcommerce.orders.read
fulfillment.status_changedcommerce.orders.read
fulfillment.updatedcommerce.orders.read
fulfillment_event.createdcommerce.orders.read
gift_card.createdcommerce.gift_cards.read
gift_card.updatedcommerce.gift_cards.read
gift_card_load.createdcommerce.gift_cards.read
gift_card_load.updatedcommerce.gift_cards.read
gift_card_notification.createdcommerce.gift_cards.read
gift_card_notification.updatedcommerce.gift_cards.read
gift_card_redemption.createdcommerce.gift_cards.read
gift_card_redemption.updatedcommerce.gift_cards.read
gift_card_transaction.createdcommerce.gift_cards.read
inventory_count.appliedcommerce.inventory.read
inventory_level.shortage_detectedcommerce.inventory.read
inventory_level.updatedcommerce.inventory.read
inventory_receipt.createdcommerce.inventory.read
inventory_reservation.action_requiredcommerce.inventory.read
inventory_reservation.at_riskcommerce.inventory.read
inventory_reservation.closedcommerce.inventory.read
inventory_reservation.committedcommerce.inventory.read
inventory_reservation.consumedcommerce.inventory.read
inventory_reservation.createdcommerce.inventory.read
inventory_reservation.hold_expiredcommerce.inventory.read
inventory_reservation.releasedcommerce.inventory.read
inventory_transfer.closedcommerce.inventory.read
inventory_transfer.departedcommerce.inventory.read
inventory_transfer.lostcommerce.inventory.read
inventory_transfer.receivedcommerce.inventory.read
inventory_transfer.returnedcommerce.inventory.read
invoice.collection_block_resolvedcommerce.invoices.read
invoice.collection_blockedcommerce.invoices.read
invoice.createdcommerce.invoices.read
invoice.creditedcommerce.invoices.read
invoice.delivery_failedcommerce.invoices.read
invoice.delivery_succeededcommerce.invoices.read
invoice.issue_failedcommerce.invoices.read
invoice.issuedcommerce.invoices.read
invoice.late_fee_assessedcommerce.invoices.read
invoice.late_fee_duecommerce.invoices.read
invoice.late_fee_waivedcommerce.invoices.read
invoice.manual_payment_recordedcommerce.invoices.read
invoice.manual_payment_reversedcommerce.invoices.read
invoice.marked_uncollectiblecommerce.invoices.read
invoice.overduecommerce.invoices.read
invoice.paidcommerce.invoices.read
invoice.partially_paidcommerce.invoices.read
invoice.partially_refundedcommerce.invoices.read
invoice.refundedcommerce.invoices.read
invoice.reminder_duecommerce.invoices.read
invoice.reopenedcommerce.invoices.read
invoice.sentcommerce.invoices.read
invoice.updatedcommerce.invoices.read
invoice.voidedcommerce.invoices.read
invoice_payment_attempt.canceledcommerce.invoices.read
invoice_payment_attempt.expiredcommerce.invoices.read
invoice_payment_attempt.failedcommerce.invoices.read
invoice_payment_attempt.processingcommerce.invoices.read
invoice_payment_attempt.settledcommerce.invoices.read
modifier_group.archivedcommerce.catalog.read
modifier_group.createdcommerce.catalog.read
modifier_group.updatedcommerce.catalog.read
modifier_set.archivedcommerce.catalog.read
modifier_set.createdcommerce.catalog.read
modifier_set.updatedcommerce.catalog.read
order.closedcommerce.orders.read
order.createdcommerce.orders.read
order.inventory_exception_createdcommerce.orders.read
order.inventory_exception_resolvedcommerce.orders.read
order.paidcommerce.orders.read
order.partially_paidcommerce.orders.read
order.payment_authorization_canceledcommerce.orders.read
order.payment_authorization_expiredcommerce.orders.read
order.payment_authorizedcommerce.orders.read
order.payment_capturedcommerce.orders.read
order.refundedcommerce.orders.read
order.updatedcommerce.orders.read
package.createdcommerce.orders.read
package.updatedcommerce.orders.read
payment_intent.canceledpayments.payment_intents.read
payment_intent.fulfillment_hold_updatedpayments.payment_intents.read
payment_intent.payment_failedpayments.payment_intents.read
payment_intent.processingpayments.payment_intents.read
payment_intent.requires_actionpayments.payment_intents.read
payment_intent.requires_capturepayments.payment_intents.read
payment_intent.succeededpayments.payment_intents.read
payment_link.createdcheckouts.payment_links.read
payment_link.updatedcheckouts.payment_links.read
payment_method.failedpayments.payment_methods.read
payment_method.removedpayments.payment_methods.read
payment_method.savedpayments.payment_methods.read
payment_review.closedrisk.read
payment_review.openedrisk.read
payment_review.reopenedrisk.read
product.archivedcommerce.products.read
product.createdcommerce.products.read
product.updatedcommerce.products.read
product_variant.archivedcommerce.products.read
product_variant.createdcommerce.products.read
product_variant.updatedcommerce.products.read
promotion.archivedcommerce.promotions.read
promotion.createdcommerce.promotions.read
promotion.updatedcommerce.promotions.read
promotion_code.archivedcommerce.promotions.read
promotion_code.createdcommerce.promotions.read
promotion_code.updatedcommerce.promotions.read
refund.createdcommerce.refunds.read
refund.failedcommerce.refunds.read
refund.updatedcommerce.refunds.read
report.failedreports.read
report.succeededreports.read
return.canceledcommerce.returns.read
return.completedcommerce.returns.read
return.createdcommerce.returns.read
return.decision_recordedcommerce.returns.read
return.reopenedcommerce.returns.read
return.updatedcommerce.returns.read
return_disposition.createdcommerce.returns.read
return_disposition.updatedcommerce.returns.read
return_inspection.acceptance_decidedcommerce.returns.read
return_inspection.createdcommerce.returns.read
return_inspection.supersededcommerce.returns.read
return_receipt.createdcommerce.returns.read
return_receipt.supersededcommerce.returns.read
return_receipt.verifiedcommerce.returns.read
return_resolution.createdcommerce.returns.read
return_resolution.updatedcommerce.returns.read
shipment.createdcommerce.orders.read
shipment.updatedcommerce.orders.read
subscription.activatedcommerce.subscriptions.read
subscription.canceledcommerce.subscriptions.read
subscription.cancellation_scheduledcommerce.subscriptions.read
subscription.createdcommerce.subscriptions.read
subscription.cycle_skippedcommerce.subscriptions.read
subscription.delivery_action_requiredcommerce.subscriptions.read
subscription.delivery_pause_upcomingcommerce.subscriptions.read
subscription.delivery_updatedcommerce.subscriptions.read
subscription.dunning_exhaustedcommerce.subscriptions.read
subscription.past_duecommerce.subscriptions.read
subscription.pausedcommerce.subscriptions.read
subscription.payment_failedcommerce.subscriptions.read
subscription.payment_succeededcommerce.subscriptions.read
subscription.reactivatedcommerce.subscriptions.read
subscription.renewal_upcomingcommerce.subscriptions.read
subscription.resumedcommerce.subscriptions.read
subscription.trial_endingcommerce.subscriptions.read
subscription.updatedcommerce.subscriptions.read
subscription_delivery_migration.completedcommerce.subscriptions.read
subscription_payment_retry.createdcommerce.subscriptions.read
subscription_payment_retry.failedcommerce.subscriptions.read
subscription_payment_retry.succeededcommerce.subscriptions.read

Inspect installs later#

App owners use these routes from their merchant auth context:

  • GET /v1/developer/partner/apps
  • GET /v1/developer/partner/apps/{partner_app_id}
  • GET /v1/developer/partner/apps/{partner_app_id}/installs
  • GET /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}/revoke
  • POST /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 state value per install attempt
  • validate state on every callback
  • exchange authorization codes only on your backend
  • keep each code_verifier with its state and send it on exchange
  • record merchant_id, partner_app_install_id, environment_grant_id, and mode
  • use webhook deliveries as the durable signal for install lifecycle changes
  • refresh tokens server-side and rotate stored refresh tokens atomically
  • test both test and live installs explicitly

Next steps#

Was this helpful?