diff --git a/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessReviewSourceDialog.tsx b/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessReviewSourceDialog.tsx
index 5c638261e..f3fe65d8e 100644
--- a/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessReviewSourceDialog.tsx
+++ b/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessReviewSourceDialog.tsx
@@ -29,6 +29,7 @@ import {
DialogContent,
DialogFooter,
DropdownItem,
+ IconArrowLink,
Input,
ThirdPartyLogo,
useDialogRef,
@@ -51,6 +52,7 @@ export const addAccessReviewSourceDialogConnectorProviderInfoFragment = graphql`
fragment AddAccessReviewSourceDialogConnectorProviderInfoFragment on ConnectorProviderInfo @relay(plural: true) {
provider
displayName
+ documentationUrl
oauthConfigured
apiKeySupported
apiKeyManaged
@@ -160,6 +162,17 @@ export function AddAccessReviewSourceDialog({
{isConnected
? (
diff --git a/e2e/console/connector_test.go b/e2e/console/connector_test.go
index e9f91c479..a9f9ae1f1 100644
--- a/e2e/console/connector_test.go
+++ b/e2e/console/connector_test.go
@@ -37,6 +37,7 @@ func TestAccessReviewDrivers(t *testing.T) {
accessReviewDrivers {
provider
displayName
+ documentationUrl
oauthConfigured
apiKeySupported
clientCredentialsSupported
@@ -51,11 +52,12 @@ func TestAccessReviewDrivers(t *testing.T) {
var result struct {
AccessReviewDrivers []struct {
- Provider string `json:"provider"`
- DisplayName string `json:"displayName"`
- OauthConfigured bool `json:"oauthConfigured"`
- APIKeySupported bool `json:"apiKeySupported"`
- ClientCredentialsSupported bool `json:"clientCredentialsSupported"`
+ Provider string `json:"provider"`
+ DisplayName string `json:"displayName"`
+ DocumentationURL *string `json:"documentationUrl"`
+ OauthConfigured bool `json:"oauthConfigured"`
+ APIKeySupported bool `json:"apiKeySupported"`
+ ClientCredentialsSupported bool `json:"clientCredentialsSupported"`
ExtraSettings []struct {
Key string `json:"key"`
Label string `json:"label"`
@@ -69,17 +71,29 @@ func TestAccessReviewDrivers(t *testing.T) {
assert.NotEmpty(t, result.AccessReviewDrivers)
providerNames := make(map[string]bool)
+ docURLByProvider := make(map[string]*string)
for _, info := range result.AccessReviewDrivers {
assert.NotEmpty(t, info.Provider)
assert.NotEmpty(t, info.DisplayName)
assert.NotNil(t, info.ExtraSettings)
providerNames[info.Provider] = true
+ docURLByProvider[info.Provider] = info.DocumentationURL
}
assert.True(t, providerNames["BREX"], "expected BREX provider to be present")
assert.True(t, providerNames["HUBSPOT"], "expected HUBSPOT provider to be present")
+ // A documented provider exposes its probo.com docs URL; an undocumented one
+ // exposes null. See pkg/connector/provider/docs.go.
+ require.Contains(t, docURLByProvider, "ANTHROPIC")
+
+ if url := docURLByProvider["ANTHROPIC"]; assert.NotNil(t, url) {
+ assert.Equal(t, "https://www.probo.com/docs/product/access-review/anthropic", *url)
+ }
+
+ assert.Nil(t, docURLByProvider["BREX"], "BREX has no doc page, documentationUrl must be null")
+
t.Run("viewer can list access review drivers", func(t *testing.T) {
t.Parallel()
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
diff --git a/pkg/connector/provider/anthropic.go b/pkg/connector/provider/anthropic.go
index 5eefe541e..3549aaac2 100644
--- a/pkg/connector/provider/anthropic.go
+++ b/pkg/connector/provider/anthropic.go
@@ -31,9 +31,10 @@ import (
func anthropicRegistration() *Registration {
return &Registration{
- Provider: coredata.ConnectorProviderAnthropic,
- DisplayName: "Anthropic",
- SupportsAPIKey: true,
+ Provider: coredata.ConnectorProviderAnthropic,
+ DisplayName: "Anthropic",
+ DocumentationURL: accessReviewDocsURL("anthropic"),
+ SupportsAPIKey: true,
// Anthropic's Admin API authenticates with the admin key in the
// x-api-key header; it rejects Authorization: Bearer and returns
// 400 when both headers are present. APIKeyHeader makes the
diff --git a/pkg/connector/provider/apollo.go b/pkg/connector/provider/apollo.go
index bd812f04f..1a108aa0d 100644
--- a/pkg/connector/provider/apollo.go
+++ b/pkg/connector/provider/apollo.go
@@ -31,9 +31,10 @@ import (
func apolloRegistration() *Registration {
return &Registration{
- Provider: coredata.ConnectorProviderApollo,
- DisplayName: "Apollo.io",
- SupportsAPIKey: true,
+ Provider: coredata.ConnectorProviderApollo,
+ DisplayName: "Apollo.io",
+ DocumentationURL: accessReviewDocsURL("apollo"),
+ SupportsAPIKey: true,
// Apollo's REST API authenticates with a master API key in the
// x-api-key header; it rejects Authorization: Bearer (and, since
// Sept 2024, query/body key params). APIKeyHeader makes the
diff --git a/pkg/connector/provider/cloudflare.go b/pkg/connector/provider/cloudflare.go
index 2b995b0bc..e6946b24e 100644
--- a/pkg/connector/provider/cloudflare.go
+++ b/pkg/connector/provider/cloudflare.go
@@ -31,10 +31,11 @@ import (
func cloudflareRegistration() *Registration {
return &Registration{
- Provider: coredata.ConnectorProviderCloudflare,
- DisplayName: "Cloudflare",
- ProbeURL: "https://api.cloudflare.com/client/v4/user/tokens/verify",
- SupportsAPIKey: true,
+ Provider: coredata.ConnectorProviderCloudflare,
+ DisplayName: "Cloudflare",
+ DocumentationURL: accessReviewDocsURL("cloudflare"),
+ ProbeURL: "https://api.cloudflare.com/client/v4/user/tokens/verify",
+ SupportsAPIKey: true,
NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
return drivers.NewCloudflareDriver(c), nil
},
diff --git a/pkg/connector/provider/crisp.go b/pkg/connector/provider/crisp.go
index fb40f78e6..87d96443e 100644
--- a/pkg/connector/provider/crisp.go
+++ b/pkg/connector/provider/crisp.go
@@ -32,8 +32,9 @@ import (
func crispRegistration() *Registration {
return &Registration{
- Provider: coredata.ConnectorProviderCrisp,
- DisplayName: "Crisp",
+ Provider: coredata.ConnectorProviderCrisp,
+ DisplayName: "Crisp",
+ DocumentationURL: accessReviewDocsURL("crisp"),
// Model B: the plugin token is Probo's own Crisp Marketplace plugin
// credential, held server-side in bootstrap config, not pasted by
// the customer. ManagedAPIKey injects it at connect time; the
diff --git a/pkg/connector/provider/cursor.go b/pkg/connector/provider/cursor.go
index 62d297cd8..7d10899b7 100644
--- a/pkg/connector/provider/cursor.go
+++ b/pkg/connector/provider/cursor.go
@@ -33,9 +33,10 @@ const cursorMembersEndpoint = "https://api.cursor.com/teams/members"
func cursorRegistration() *Registration {
return &Registration{
- Provider: coredata.ConnectorProviderCursor,
- DisplayName: "Cursor",
- SupportsAPIKey: true,
+ Provider: coredata.ConnectorProviderCursor,
+ DisplayName: "Cursor",
+ DocumentationURL: accessReviewDocsURL("cursor"),
+ SupportsAPIKey: true,
// Cursor's Admin API has no third-party OAuth2 flow; it
// authenticates with a team admin key (key_...) presented as the
// HTTP Basic auth username with an empty password ("-u :")
diff --git a/pkg/connector/provider/deepgram.go b/pkg/connector/provider/deepgram.go
index 3c0ae6348..e630ff54a 100644
--- a/pkg/connector/provider/deepgram.go
+++ b/pkg/connector/provider/deepgram.go
@@ -31,9 +31,10 @@ import (
func deepgramRegistration() *Registration {
return &Registration{
- Provider: coredata.ConnectorProviderDeepgram,
- DisplayName: "Deepgram",
- SupportsAPIKey: true,
+ Provider: coredata.ConnectorProviderDeepgram,
+ DisplayName: "Deepgram",
+ DocumentationURL: accessReviewDocsURL("deepgram"),
+ SupportsAPIKey: true,
// Deepgram authenticates with an API key under the `Token` scheme
// (`Authorization: Token `), not Bearer. APIKeyAuthScheme makes
// the APIKeyConnection use that scheme. There is no third-party
diff --git a/pkg/connector/provider/docs.go b/pkg/connector/provider/docs.go
new file mode 100644
index 000000000..bf6684707
--- /dev/null
+++ b/pkg/connector/provider/docs.go
@@ -0,0 +1,33 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package provider
+
+// accessReviewDocsBaseURL is the public probo.com docs root for access-review
+// connectors; each documented provider's page lives at this base + its slug.
+const accessReviewDocsBaseURL = "https://www.probo.com/docs/product/access-review/"
+
+// accessReviewDocsURL builds the public documentation URL for an access-review
+// connector from its page slug (e.g. "anthropic"). The slug is passed explicitly
+// by each registration rather than derived from the provider enum, so an enum
+// containing an underscore (e.g. BETTER_STACK) cannot produce a wrong URL.
+func accessReviewDocsURL(slug string) string {
+ return accessReviewDocsBaseURL + slug
+}
diff --git a/pkg/connector/provider/openai.go b/pkg/connector/provider/openai.go
index 6e18972e7..95e9deb1b 100644
--- a/pkg/connector/provider/openai.go
+++ b/pkg/connector/provider/openai.go
@@ -31,10 +31,11 @@ import (
func openaiRegistration() *Registration {
return &Registration{
- Provider: coredata.ConnectorProviderOpenAI,
- DisplayName: "OpenAI",
- ProbeURL: "https://api.openai.com/v1/models",
- SupportsAPIKey: true,
+ Provider: coredata.ConnectorProviderOpenAI,
+ DisplayName: "OpenAI",
+ DocumentationURL: accessReviewDocsURL("openai"),
+ ProbeURL: "https://api.openai.com/v1/models",
+ SupportsAPIKey: true,
NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
return drivers.NewOpenAIDriver(c), nil
},
diff --git a/pkg/connector/provider/openrouter.go b/pkg/connector/provider/openrouter.go
index 1a795a216..79109bdb1 100644
--- a/pkg/connector/provider/openrouter.go
+++ b/pkg/connector/provider/openrouter.go
@@ -31,9 +31,10 @@ import (
func openrouterRegistration() *Registration {
return &Registration{
- Provider: coredata.ConnectorProviderOpenRouter,
- DisplayName: "OpenRouter",
- SupportsAPIKey: true,
+ Provider: coredata.ConnectorProviderOpenRouter,
+ DisplayName: "OpenRouter",
+ DocumentationURL: accessReviewDocsURL("openrouter"),
+ SupportsAPIKey: true,
// OpenRouter authenticates with an organization management
// (provisioning) API key presented as Authorization: Bearer, the
// default APIKeyConnection scheme. The key is bound to one
diff --git a/pkg/connector/provider/railway.go b/pkg/connector/provider/railway.go
index 4f4054073..68d719685 100644
--- a/pkg/connector/provider/railway.go
+++ b/pkg/connector/provider/railway.go
@@ -31,9 +31,10 @@ import (
func railwayRegistration() *Registration {
return &Registration{
- Provider: coredata.ConnectorProviderRailway,
- DisplayName: "Railway",
- SupportsAPIKey: true,
+ Provider: coredata.ConnectorProviderRailway,
+ DisplayName: "Railway",
+ DocumentationURL: accessReviewDocsURL("railway"),
+ SupportsAPIKey: true,
// Railway authenticates with an account API token as Authorization:
// Bearer. A single GraphQL call resolves the account's workspaces and
// their members, so there is nothing to pick (Pattern 3). Railway
diff --git a/pkg/connector/provider/resend.go b/pkg/connector/provider/resend.go
index 82fd069bd..7a72c8b39 100644
--- a/pkg/connector/provider/resend.go
+++ b/pkg/connector/provider/resend.go
@@ -31,10 +31,11 @@ import (
func resendRegistration() *Registration {
return &Registration{
- Provider: coredata.ConnectorProviderResend,
- DisplayName: "Resend",
- ProbeURL: "https://api.resend.com/domains",
- SupportsAPIKey: true,
+ Provider: coredata.ConnectorProviderResend,
+ DisplayName: "Resend",
+ DocumentationURL: accessReviewDocsURL("resend"),
+ ProbeURL: "https://api.resend.com/domains",
+ SupportsAPIKey: true,
NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
return drivers.NewResendDriver(c), nil
},
diff --git a/pkg/connector/provider/scaleway.go b/pkg/connector/provider/scaleway.go
index 5d7ffb328..48b2991f3 100644
--- a/pkg/connector/provider/scaleway.go
+++ b/pkg/connector/provider/scaleway.go
@@ -32,9 +32,10 @@ import (
func scalewayRegistration() *Registration {
return &Registration{
- Provider: coredata.ConnectorProviderScaleway,
- DisplayName: "Scaleway",
- SupportsAPIKey: true,
+ Provider: coredata.ConnectorProviderScaleway,
+ DisplayName: "Scaleway",
+ DocumentationURL: accessReviewDocsURL("scaleway"),
+ SupportsAPIKey: true,
// Scaleway authenticates with the secret key in the X-Auth-Token header
// rather than Authorization: Bearer. APIKeyHeader makes the
// APIKeyConnection send that header and omit Authorization. The key is
diff --git a/pkg/connector/provider/types.go b/pkg/connector/provider/types.go
index 9050c88d1..96371d2e4 100644
--- a/pkg/connector/provider/types.go
+++ b/pkg/connector/provider/types.go
@@ -39,6 +39,10 @@ type Registration struct {
// Identity.
Provider coredata.ConnectorProvider
DisplayName string
+ // DocumentationURL is the public probo.com docs page for connecting this
+ // provider as an access source. Empty for providers with no doc page yet;
+ // surfaced (nullable) on ConnectorProviderInfo so the console renders a link.
+ DocumentationURL string
// OAuth2 metadata.
AuthURL string
diff --git a/pkg/connector/provider/yousign.go b/pkg/connector/provider/yousign.go
index 0636c6bb3..009710bcf 100644
--- a/pkg/connector/provider/yousign.go
+++ b/pkg/connector/provider/yousign.go
@@ -31,9 +31,10 @@ import (
func yousignRegistration() *Registration {
return &Registration{
- Provider: coredata.ConnectorProviderYousign,
- DisplayName: "Yousign",
- SupportsAPIKey: true,
+ Provider: coredata.ConnectorProviderYousign,
+ DisplayName: "Yousign",
+ DocumentationURL: accessReviewDocsURL("yousign"),
+ SupportsAPIKey: true,
// Yousign authenticates with an API key as Authorization: Bearer. The
// key is bound to one organization, so GET /v3/users returns everyone
// with nothing to pick (Pattern 3). The connector targets Yousign
diff --git a/pkg/server/api/console/v1/base_resolvers.go b/pkg/server/api/console/v1/base_resolvers.go
index 72b7d4363..7a9894548 100644
--- a/pkg/server/api/console/v1/base_resolvers.go
+++ b/pkg/server/api/console/v1/base_resolvers.go
@@ -607,9 +607,15 @@ func (r *queryResolver) AccessReviewDrivers(ctx context.Context) ([]*types.Conne
)
}
+ var documentationURL *string
+ if reg.DocumentationURL != "" {
+ documentationURL = new(reg.DocumentationURL)
+ }
+
infos = append(infos, &types.ConnectorProviderInfo{
Provider: provider,
DisplayName: reg.DisplayName,
+ DocumentationURL: documentationURL,
OauthConfigured: oauthConfigured,
APIKeySupported: apiKeySupported,
APIKeyManaged: apiKeyManaged,
diff --git a/pkg/server/api/console/v1/graphql/connector.graphql b/pkg/server/api/console/v1/graphql/connector.graphql
index cdf4c16b3..1193fb7ab 100644
--- a/pkg/server/api/console/v1/graphql/connector.graphql
+++ b/pkg/server/api/console/v1/graphql/connector.graphql
@@ -104,6 +104,11 @@ enum ConnectorProvider
type ConnectorProviderInfo {
provider: ConnectorProvider!
displayName: String!
+ """
+ documentationUrl is the public probo.com docs page for connecting this
+ provider as an access source, or null when no doc page exists yet.
+ """
+ documentationUrl: String
oauthConfigured: Boolean!
apiKeySupported: Boolean!
"""