Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/api-catalog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> Compact provider slug catalog for agents. Use the slug to reconstruct provider-specific docs URLs only when needed.

Provider count: 874
Provider count: 875

URL patterns:
- Main provider page: https://nango.dev/docs/integrations/all/{slug}.md or https://nango.dev/docs/api-integrations/{slug}.md
Expand Down Expand Up @@ -658,6 +658,7 @@ URL patterns:
| `sage-intacct` | Sage Intacct | TWO_STEP | [docs](https://nango.dev/docs/integrations/all/sage-intacct.md) | [connect](https://nango.dev/docs/integrations/all/sage-intacct/connect.md) | | accounting, erp |
| `sage-intacct-cc` | Sage Intacct (Client Credentials) | TWO_STEP | [docs](https://nango.dev/docs/api-integrations/sage-intacct-cc.md) | [connect](https://nango.dev/docs/api-integrations/sage-intacct-cc/connect.md) | [setup](https://nango.dev/docs/api-integrations/sage-intacct-cc/connect.md) | accounting, erp |
| `sage-intacct-oauth` | Sage Intacct (OAuth) | OAUTH2 | [docs](https://nango.dev/docs/integrations/all/sage-intacct-oauth.md) | | | accounting, erp, popular |
| `sage-member` | SAGE Member | API_KEY | [docs](https://nango.dev/docs/api-integrations/sage-member.md) | [connect](https://nango.dev/docs/api-integrations/sage-member/connect.md) | | marketing, e-commerce |
| `sage-people` | Sage People | OAUTH2 | [docs](https://nango.dev/docs/integrations/all/sage-people.md) | [connect](https://nango.dev/docs/integrations/all/sage-people/connect.md) | | hr |
| `salesforce` | Salesforce | OAUTH2 | [docs](https://nango.dev/docs/api-integrations/salesforce.md) | [connect](https://nango.dev/docs/api-integrations/salesforce/connect.md) | [setup](https://nango.dev/docs/api-integrations/salesforce/salesforce-api-oauth-app-setup.md) | crm, popular |
| `salesforce-cc` | Salesforce (Client Credentials) | OAUTH2_CC | [docs](https://nango.dev/docs/api-integrations/salesforce-cc.md) | [connect](https://nango.dev/docs/api-integrations/salesforce-cc/connect.md) | | crm |
Expand Down
87 changes: 87 additions & 0 deletions docs/api-integrations/sage-member.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
title: 'SAGE Member'
sidebarTitle: 'SAGE Member'
description: 'Integrate your application with the SAGE Member API'
---

## Quickstart

Connect to SAGE Member with Nango and see data flow in 2 minutes.

<Steps>
<Step title="Create the integration">
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
In Nango ([free signup](https://app.nango.dev)), go to [Integrations](https://app.nango.dev/dev/integrations) -> _Configure New Integration_ -> _SAGE Member_.
</Step>
<Step title="Authorize SAGE Member">
Go to [Connections](https://app.nango.dev/dev/connections) -> _Add Test Connection_ -> _Authorize_, then enter your Account Number, Login ID, and Authentication Key. Later, you'll let your users do the same directly from your app.
</Step>
<Step title="Call the SAGE Member API">
Let's make your first request to the SAGE Member API. Replace the placeholders below with your [secret key](https://app.nango.dev/dev/environment-settings), [integration ID](https://app.nango.dev/dev/integrations), and [connection ID](https://app.nango.dev/dev/connections):
<Tabs>
<Tab title="cURL">

```bash
curl -X POST "https://api.nango.dev/proxy/ConnectAPI" \
-H "Authorization: Bearer <NANGO-SECRET-KEY>" \
-H "Provider-Config-Key: <INTEGRATION-ID>" \
-H "Connection-Id: <CONNECTION-ID>" \
-H "Content-Type: application/json" \
-d '{"serviceId": 101, "apiVer": 130}'
```

</Tab>

<Tab title="Node">

Install Nango's backend SDK with `npm i @nangohq/node`. Then run:

```typescript
import { Nango } from '@nangohq/node';

const nango = new Nango({ secretKey: '<NANGO-SECRET-KEY>' });

const res = await nango.post({
endpoint: '/ConnectAPI',
providerConfigKey: '<INTEGRATION-ID>',
connectionId: '<CONNECTION-ID>',
data: {
serviceId: 101,
apiVer: 130
}
});

console.log(res.data);
```
</Tab>

</Tabs>
Nango automatically injects your Account Number, Login ID, and Authentication Key into the `auth` object of every proxied request body — you only need to supply the `serviceId` and other fields for the specific SAGE Member module you're calling.

Or fetch credentials with the [Node SDK](/reference/sdks/node#get-a-connection-with-credentials) or [API](/reference/api/connection/get).

You're connected! Check the [Logs](https://app.nango.dev/dev/logs) tab in Nango to inspect requests.
</Step>

<Step title="Implement Nango in your app">
Follow our [Auth implementation guide](/guides/primitives/auth) to integrate Nango in your app.

To obtain your own production credentials, follow the setup guide linked below.
</Step>
</Steps>

## SAGE Member Integration Guides
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated

Nango-maintained guides for common use cases.

- [How do I link my account?](/api-integrations/sage-member/connect)
Connect your SAGE Member account using the Connect UI

## Pre-built syncs & actions for SAGE Member

Enable them in your dashboard. Extend and customize to fit your needs.

import PreBuiltUseCases from "/snippets/generated/sage-member/PreBuiltUseCases.mdx"

<PreBuiltUseCases />

---
40 changes: 40 additions & 0 deletions docs/api-integrations/sage-member/connect.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: SAGE Member - How do I link my account?
sidebarTitle: SAGE Member
---

# Overview

To authenticate with SAGE Member, you need:
1. **Account Number** — Your SAGE Member account number.
2. **Login ID** — The login ID of the SAGE Member user account making the requests.
3. **Authentication Key** — The key associated with that user's login, used to authenticate every request.

### Prerequisites

- An active SAGE Workplace subscription or Supplier Advantage membership.
- A SAGE Member module enabled on your account (enable modules on the Services tab).

### Step 1: Finding your credentials

SAGE Member authenticates on a per-user basis, so each set of credentials is tied to one user on your account.

1. Log in to your SAGE account and go to **Developer APIs** > **SAGE Connect**.
2. Go to the **Users** tab.
3. Click your profile in the top right to find your **Account Number**.
4. Select the user that will be making API requests and note their **Login ID** and **Authentication Key**.

<img src="/api-integrations/sage-member/users_tab.png"/>
Comment thread
hassan254-prog marked this conversation as resolved.


### Step 2: Enter credentials in the Connect UI

Once you have your credentials:
1. Open the form where you need to authenticate with SAGE Member.
2. Enter your **Account Number**, **Login ID**, and **Authentication Key** in their designated fields.
3. Submit the form, and you should be successfully authenticated.

<img src="/api-integrations/sage-member/form.png" style={{maxWidth: "450px" }}/>
Comment thread
hassan254-prog marked this conversation as resolved.


You are now connected to SAGE Member.
Binary file added docs/api-integrations/sage-member/users_tab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,7 @@
"integrations/all/sage-intacct",
"integrations/all/sage-intacct-oauth",
"api-integrations/sage-intacct-cc",
"api-integrations/sage-member",
"integrations/all/sage-people",
"api-integrations/sap-ariba",
"api-integrations/salesforce",
Expand Down
1 change: 1 addition & 0 deletions docs/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14560,6 +14560,7 @@ Use these slugs to construct provider-specific docs URLs only when needed.
| `sage-intacct` | Sage Intacct | TWO_STEP | [docs](https://nango.dev/docs/integrations/all/sage-intacct.md) | [connect](https://nango.dev/docs/integrations/all/sage-intacct/connect.md) | | accounting, erp |
| `sage-intacct-cc` | Sage Intacct (Client Credentials) | TWO_STEP | [docs](https://nango.dev/docs/api-integrations/sage-intacct-cc.md) | [connect](https://nango.dev/docs/api-integrations/sage-intacct-cc/connect.md) | [setup](https://nango.dev/docs/api-integrations/sage-intacct-cc/connect.md) | accounting, erp |
| `sage-intacct-oauth` | Sage Intacct (OAuth) | OAUTH2 | [docs](https://nango.dev/docs/integrations/all/sage-intacct-oauth.md) | | | accounting, erp, popular |
| `sage-member` | SAGE Member | API_KEY | [docs](https://nango.dev/docs/api-integrations/sage-member.md) | [connect](https://nango.dev/docs/api-integrations/sage-member/connect.md) | | marketing, e-commerce |
| `sage-people` | Sage People | OAUTH2 | [docs](https://nango.dev/docs/integrations/all/sage-people.md) | [connect](https://nango.dev/docs/integrations/all/sage-people/connect.md) | | hr |
| `salesforce` | Salesforce | OAUTH2 | [docs](https://nango.dev/docs/api-integrations/salesforce.md) | [connect](https://nango.dev/docs/api-integrations/salesforce/connect.md) | [setup](https://nango.dev/docs/api-integrations/salesforce/salesforce-api-oauth-app-setup.md) | crm, popular |
| `salesforce-cc` | Salesforce (Client Credentials) | OAUTH2_CC | [docs](https://nango.dev/docs/api-integrations/salesforce-cc.md) | [connect](https://nango.dev/docs/api-integrations/salesforce-cc/connect.md) | | crm |
Expand Down
2 changes: 1 addition & 1 deletion docs/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ Use provider URLs by replacing `{slug}` with a slug from the API catalog:

## APIs and integrations

- [API catalog](https://nango.dev/docs/api-catalog.txt): 874 provider slugs with canonical docs routes, auth modes, setup guides, and connect guides.
- [API catalog](https://nango.dev/docs/api-catalog.txt): 875 provider slugs with canonical docs routes, auth modes, setup guides, and connect guides.
- Provider-specific pages are intentionally not expanded here so core Nango guides remain easy for agents to find.

## Resources
Expand Down
3 changes: 3 additions & 0 deletions docs/snippets/generated/sage-member/PreBuiltUseCases.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
_No pre-built syncs or actions available yet._

<Tip>Not seeing the integration you need? [Build your own](/guides/functions/functions-guide) independently.</Tip>
40 changes: 40 additions & 0 deletions packages/providers/providers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17633,6 +17633,46 @@ sage-intacct:
description: Your Sage Intacct User Password
secret: true

sage-member:
display_name: SAGE Member
categories:
- marketing
- e-commerce
auth_mode: API_KEY
proxy:
base_url: https://www.promoplace.com/ws/ws.dll

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that doesn't look like SAGE related?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what they have in their gated docs.
Screenshot 2026-07-10 at 17 17 38

body:
auth:
acctId: ${connectionConfig.acctId}
loginId: ${connectionConfig.loginId}
key: ${apiKey}
docs: https://nango.dev/docs/api-integrations/sage-member
docs_connect: https://nango.dev/docs/api-integrations/sage-member/connect
credentials:
apiKey:
type: string
title: Authentication Key
example: 56edbbe3********************141a
pattern: '^[a-f0-9]{32}$'
description: The authentication key for your SAGE Member user.
secret: true
doc_section: '#step-1-finding-your-credentials'
connection_config:
acctId:
type: string
title: Account Number
description: Your SAGE Member account number.
pattern: '^[0-9]+$'
example: '4353478'
order: 1
doc_section: '#step-1-finding-your-credentials'
loginId:
type: string
title: Login ID
description: The login ID for your SAGE Member user.
order: 2
doc_section: '#step-1-finding-your-credentials'

sage-people:
display_name: Sage People
categories:
Expand Down
44 changes: 35 additions & 9 deletions packages/shared/lib/services/proxy/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,25 +509,47 @@ export function buildProxyBody({
}: {
config: ApplicationConstructedProxyConfiguration;
connection: ConnectionForProxy;
}): Record<string, string> | null {
}): Record<string, string | Record<string, string>> | null {
if (!config.provider?.proxy?.body) {
return null;
}

const body: Record<string, string> = {};
const replacers = {
connectionConfig: connection.connection_config,
credentials: connection.credentials,
...(connection.credentials as unknown as Record<string, string>)
};

const interpolateLeaf = (value: string): string | null => {
const interpolated = interpolateIfNeeded(value, replacers);
return interpolated.includes('${') ? null : interpolated;
};

const body: Record<string, string | Record<string, string>> = {};
for (const [key, value] of Object.entries(config.provider.proxy.body)) {
if (typeof value !== 'string') {
if (typeof value === 'string') {
const interpolated = interpolateLeaf(value);
if (interpolated !== null) {
body[key] = interpolated;
}
continue;
}
const interpolated = interpolateIfNeeded(value, replacers);
if (!interpolated.includes('${')) {
body[key] = interpolated;

if (!isPlainObject(value)) {
continue;
}
const nested: Record<string, string> = {};
for (const [nestedKey, nestedValue] of Object.entries(value)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if there is more than one level of nesting?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated this in the latest commit to handle recursive object mapping instead of only supporting a single level of nested objects.

if (typeof nestedValue !== 'string') {
continue;
}
const interpolated = interpolateLeaf(nestedValue);
if (interpolated !== null) {
nested[nestedKey] = interpolated;
}
}
if (Object.keys(nested).length > 0) {
body[key] = nested;
}
}

Expand All @@ -545,15 +567,19 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
);
}

function mergeInjectedBody(existing: unknown, injected: Record<string, string>): unknown {
function mergeInjectedBody(existing: unknown, injected: Record<string, string | Record<string, string>>): unknown {
if (!existing) return injected;
if (isPlainObject(existing)) return { ...existing, ...injected };
if (existing instanceof FormData) {
for (const [key, value] of Object.entries(injected)) existing.append(key, value);
for (const [key, value] of Object.entries(injected)) {
if (typeof value === 'string') existing.append(key, value);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
}
return existing;
}
if (existing instanceof URLSearchParams) {
for (const [key, value] of Object.entries(injected)) existing.append(key, value);
for (const [key, value] of Object.entries(injected)) {
if (typeof value === 'string') existing.append(key, value);
}
return existing;
}
return existing;
Expand Down
85 changes: 85 additions & 0 deletions packages/shared/lib/services/proxy/utils.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2183,4 +2183,89 @@ describe('buildProxyBody', () => {
const result = buildProxyBody({ config, connection: getTestConnection() });
expect(result).toEqual({ label: 'static' });
});

it('builds a nested object from a nested proxy.body map', () => {
const config = getDefaultProxy({
provider: {
auth_mode: 'API_KEY',
proxy: {
base_url: 'https://example.com',
body: {
auth: {
acctId: '${connectionConfig.acctId}',
loginId: '${connectionConfig.loginId}',
key: '${apiKey}'
}
}
}
}
});
const result = buildProxyBody({
config,
connection: getTestConnection({
credentials: { type: 'API_KEY', apiKey: 'my-secret-key' },
connection_config: { acctId: '26878', loginId: 'TestUser' }
})
});
expect(result).toEqual({ auth: { acctId: '26878', loginId: 'TestUser', key: 'my-secret-key' } });
});

it('drops unresolved keys from a nested proxy.body map but keeps the resolved ones', () => {
const config = getDefaultProxy({
provider: {
auth_mode: 'API_KEY',
proxy: {
base_url: 'https://example.com',
body: {
auth: {
acctId: '${connectionConfig.acctId}',
key: '${apiKey}'
}
}
}
}
});
const result = buildProxyBody({
config,
connection: getTestConnection({ credentials: { type: 'API_KEY', apiKey: 'my-secret-key' }, connection_config: {} })
});
expect(result).toEqual({ auth: { key: 'my-secret-key' } });
});

it('omits a nested map entirely when none of its keys resolve', () => {
const config = getDefaultProxy({
provider: {
auth_mode: 'API_KEY',
proxy: {
base_url: 'https://example.com',
body: { auth: { acctId: '${connectionConfig.acctId}' } }
}
}
});
const result = buildProxyBody({
config,
connection: getTestConnection({ connection_config: {} })
});
expect(result).toBeNull();
});

it('mixes flat and nested values in the same proxy.body map', () => {
const config = getDefaultProxy({
provider: {
auth_mode: 'API_KEY',
proxy: {
base_url: 'https://example.com',
body: {
serviceId: '101',
auth: { key: '${apiKey}' }
}
}
}
});
const result = buildProxyBody({
config,
connection: getTestConnection({ credentials: { type: 'API_KEY', apiKey: 'my-secret-key' } })
});
expect(result).toEqual({ serviceId: '101', auth: { key: 'my-secret-key' } });
});
});
2 changes: 1 addition & 1 deletion packages/types/lib/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export interface BaseProvider {
headers?: Record<string, string>;
connection_config?: Record<string, string>;
query?: Record<string, string>;
body?: Record<string, string>;
body?: Record<string, string | Record<string, string>>;
retry?: RetryHeaderConfig;
decompress?: boolean;
forward_headers_on_redirect?: boolean;
Expand Down
Loading
Loading