Partner app installs

Use this guide when you want merchants to 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 for a partner install access token
  5. call normal Flint /v1/... APIs with that token
  6. 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_token or an external API key with merchants.profile.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#

Bash
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

client_secret is shown once. Flint cannot return the same value later.

Step 2: build the hosted install URL#

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
  • optional permission_ids
  • optional environment_id

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&permission_ids=manage_orders,read_customers

Rules to remember:

  • redirect_uri must exactly match a registered redirect URI
  • if permission_ids is omitted, Flint uses default_requested_permissions
  • all non-optional permissions are always granted
  • if environment_id is omitted, Flint uses the merchant's default environment for the selected mode
  • live installs require the merchant to have completed onboarding

Optional: preview before redirecting#

If you want to validate the request or show a preflight summary in your own product, call:

Bash
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&permission_ids=manage_orders,read_customers"

The preview response returns the app metadata plus the exact permission set Flint will request from the merchant.

Step 3: handle the callback#

On approval, 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=merchant+denied+install&state=merchant-session-42

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.

Bash
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"

Example response:

JSON
{
  "access_token": "eyJ...",
  "token_type": "bearer",
  "expires_in": 3600,
  "refresh_token": "fprt_abc123",
  "scope": "commerce.orders.read commerce.orders.write customers.read",
  "merchant_id": "mer_123",
  "partner_app_id": "papp_123",
  "partner_app_install_id": "pinst_123",
  "environment_grant_id": "egrt_123",
  "mode": "test"
}

Store:

  • refresh_token
  • merchant_id
  • partner_app_install_id
  • environment_grant_id
  • mode
  • granted scope

Step 5: call Flint APIs with the install token#

Use the returned access token on the normal Flint public API.

Bash
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, POST /v1/merchant-account-sessions, and GET /v1/capabilities reject partner install tokens with 401 ONBOARDING_PARTNER_AUTH_UNSUPPORTED. A partner install with merchants.profile.read can read the Merchant resource, including its payment and payout readiness. 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:

Bash
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.

Step 7: subscribe to partner webhooks#

The current webhook model is one endpoint per partner app. When you create that endpoint, choose one pattern:

  • app lifecycle webhooks, to track installs and revocations
  • installed-merchant event webhooks, to receive Flint resource events for installed merchants

Both patterns 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.

Option A: app lifecycle webhook#

Bash
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",
    "mode": "both",
    "enabled": true
  }'

If enabled_events is omitted for event_sources: ["partner_app"], Flint subscribes you to all lifecycle events:

  • partner_app.install.created
  • partner_app.install.updated
  • partner_app.install.permissions_updated
  • partner_app.install.revoked
  • partner_app.install.environment_grant.created
  • partner_app.install.environment_grant.revoked

Lifecycle events require mode: "both".

Option B: Installed-merchant webhook#

Bash
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",
    "mode": "both",
    "enabled_events": [
      "order.paid",
      "payment_intent.succeeded",
      "refund.created"
    ],
    "enabled": true
  }'

For event_sources: ["installed_merchants"]:

  • enabled_events requires at least one installed-merchant event
  • mode must be test, live, or both, and applies to the events Flint forwards
  • 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

Read them 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.

If you need both lifecycle events and installed-merchant event forwarding today, you need to choose which category Flint sends directly for that app and fill the other gap from your own control plane or install inspection routes.

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
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.allocation_createdcommerce.credit_notes.read
credit_note.allocation_reversedcommerce.credit_notes.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
customer.createdcustomers.read
customer.deletion_completedcustomers.read
customer.deletion_rejectedcustomers.read
customer.deletion_requestedcustomers.read
customer.updatedcustomers.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.archivedcommerce.delivery.read
delivery_rate.createdcommerce.delivery.read
delivery_rate.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
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_duecommerce.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.payment_attempt_canceledcommerce.invoices.read
invoice.payment_attempt_expiredcommerce.invoices.read
invoice.payment_failedcommerce.invoices.read
invoice.payment_processingcommerce.invoices.read
invoice.refundedcommerce.invoices.read
invoice.reminder_duecommerce.invoices.read
invoice.sentcommerce.invoices.read
invoice.updatedcommerce.invoices.read
invoice.voidedcommerce.invoices.read
order.closedcommerce.orders.read
order.createdcommerce.orders.read
order.fulfillment.completedcommerce.orders.read
order.fulfillment.createdcommerce.orders.read
order.fulfillment.event.createdcommerce.orders.read
order.fulfillment.package.createdcommerce.orders.read
order.fulfillment.package.updatedcommerce.orders.read
order.fulfillment.shipment.createdcommerce.orders.read
order.fulfillment.shipment.updatedcommerce.orders.read
order.fulfillment.status_changedcommerce.orders.read
order.fulfillment.updatedcommerce.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
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_method.failedpayments.payment_methods.read
payment_method.removedpayments.payment_methods.read
payment_method.savedpayments.payment_methods.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
review.closedrisk.read
review.openedrisk.read
subscription.activatedcommerce.subscriptions.read
subscription.canceledcommerce.subscriptions.read
subscription.createdcommerce.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.renewal_upcomingcommerce.subscriptions.read
subscription.resumedcommerce.subscriptions.read
subscription.trial_endingcommerce.subscriptions.read
subscription.updatedcommerce.subscriptions.read

Inspect installs later#

Use these routes from your 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}

Use revocation routes 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

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
  • 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#

Rate this doc