> ## Documentation Index
> Fetch the complete documentation index at: https://docs.profy.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# App Development Guide

> Build an external application integrated with Profy identity and billing

An App is an external application that integrates with Profy's identity and billing system through OAuth + Events API. As an App developer, you can leverage Profy's user system and credit-based billing to quickly monetize your product.

## What Is a Profy App

| Aspect               | Expert                             | App                                |
| -------------------- | ---------------------------------- | ---------------------------------- |
| **Runtime**          | Runs within the Profy platform     | Self-deployed by the developer     |
| **User Interaction** | Through Profy's chat interface     | Custom interface                   |
| **Integration**      | Persona/tools/skills configuration | OAuth + API integration            |
| **Billing**          | METERED (per token)                | METERED + PER\_USE (custom events) |
| **Distribution**     | Profy Expert Marketplace           | Profy App Marketplace              |

## Development Workflow

<Steps>
  <Step title="Create an App">
    Create a new App in Profy Studio. You will receive:

    * **App UUID** (i.e., `client_id`)
    * **App Secret** (i.e., `client_secret`, displayed only once)
    * **OAuth configuration entry**

    Configure OAuth settings in your App:

    * Register one or more **Redirect URIs** (callback URLs)
    * Set the App's name, description, and icon
  </Step>

  <Step title="Implement the OAuth Flow">
    Implement the standard OAuth 2.0 Authorization Code flow in your application:

    ```typescript theme={null}
    // 1. Redirect the user to authorize
    const authUrl = new URL("https://api.profy.cn/oauth/authorize");
    authUrl.searchParams.set("client_id", APP_UUID);
    authUrl.searchParams.set("redirect_uri", "https://your-app.com/callback");
    authUrl.searchParams.set("response_type", "code");
    authUrl.searchParams.set("scope", "events:write");
    authUrl.searchParams.set("state", generateRandomState());
    // Redirect the user to authUrl

    // 2. Exchange the code for a token in the callback
    const tokenResp = await fetch("https://api.profy.cn/oauth/token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        grant_type: "authorization_code",
        code: callbackCode,
        redirect_uri: "https://your-app.com/callback",
        client_id: APP_UUID,
        client_secret: APP_SECRET,
      }),
    });
    const { access_token, refresh_token } = await tokenResp.json();
    ```

    For the full flow, see [OAuth Integration](/en/developers/oauth-integration).
  </Step>

  <Step title="Configure Billing Meters">
    If your App needs per-use billing (PER\_USE), configure Meters:

    1. Add a Meter in your App settings
    2. Define the event name (e.g., `image_generation`)
    3. Set the credit price per event
    4. Save the configuration

    Meter configurations can be queried via the `GET /v1/meters` endpoint.
  </Step>

  <Step title="Integrate the API">
    Use the OAuth Token to call the Profy API:

    **Call an AI model** (METERED billing):

    ```typescript theme={null}
    const resp = await fetch("https://api.profy.cn/v1/chat/completions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${access_token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model: "deepseek-v3",
        messages: [{ role: "user", content: userMessage }],
      }),
    });
    ```

    **Report a custom event** (PER\_USE billing):

    ```typescript theme={null}
    const resp = await fetch("https://api.profy.cn/v1/events", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${access_token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        event: "image_generation",
        idempotency_key: `${userId}_${taskId}`,
      }),
    });
    ```
  </Step>

  <Step title="Submit for Review">
    Once your App is complete, submit it for review to publish on the Profy App Marketplace:

    1. Complete the App information in Studio (name, description, icon, screenshots)
    2. Click the "Publish" button to submit for review
    3. The review team will check the App's functionality and security
    4. After approval, the App goes live on the marketplace
  </Step>

  <Step title="Publish to the Marketplace">
    After approval, your App will appear in the Profy App Marketplace. Users can discover, authorize, and use your App.

    Credit revenue earned through usage will be distributed according to the creator incentive program.
  </Step>
</Steps>

## Billing Models

Apps support two billing models that can be combined:

### METERED (Per-Token Billing)

Suitable for AI model invocation scenarios. When users call the Chat Completions API, credits are deducted from the user's account based on actual token consumption.

```typescript theme={null}
// Credits are automatically deducted based on token usage
const resp = await fetch("https://api.profy.cn/v1/chat/completions", {
  method: "POST",
  headers: { Authorization: `Bearer ${access_token}`, ...},
  body: JSON.stringify({ model: "deepseek-v3", messages: [...] }),
});
```

### PER\_USE (Per-Event Billing)

Suitable for custom feature scenarios. Report events through the Events API, and credits are deducted at the fixed price configured in the Meter.

```typescript theme={null}
// Deducted at the fixed price configured in the Meter
const resp = await fetch("https://api.profy.cn/v1/events", {
  method: "POST",
  headers: { Authorization: `Bearer ${access_token}`, ...},
  body: JSON.stringify({
    event: "premium_feature",
    idempotency_key: uniqueKey,
  }),
});
```

## App Review Process

App publishing uses a dual-track review mechanism (consistent with Expert review):

| Scenario               | Behavior                                                     |
| ---------------------- | ------------------------------------------------------------ |
| **First publication**  | App enters `PENDING_REVIEW` status; goes live after approval |
| **Version update**     | Old version stays online; new version enters review          |
| **Review rejected**    | Can be modified and resubmitted                              |
| **Creator withdrawal** | Pending submissions can be withdrawn                         |

Review focus areas:

* Whether the App is fully functional
* Whether the OAuth flow meets security standards
* Whether the user experience is reasonable
* Whether billing is transparent

## OAuth and App Status

The OAuth authorization flow is available as soon as the App is created, **regardless of `application.status`**. The `status` only controls discoverability in the marketplace:

| Status           | OAuth       | Marketplace Visibility |
| ---------------- | ----------- | ---------------------- |
| `OFFLINE`        | ✅ Available | ❌ Not visible          |
| `PENDING_REVIEW` | ✅ Available | ❌ Not visible          |
| `ONLINE`         | ✅ Available | ✅ Visible              |

This means you can complete OAuth integration and testing before the App goes live.

## Full Integration Example

Here is a complete App backend example built with Express + Profy SDK:

```typescript theme={null}
import express from "express";
import { Profy } from "@profy-ai/sdk";

const app = express();
app.use(express.json());

const APP_UUID = process.env.PROFY_APP_UUID!;
const APP_SECRET = process.env.PROFY_APP_SECRET!;

// 1. Initiate OAuth authorization
app.get("/auth/profy", (req, res) => {
  const state = crypto.randomUUID();
  // Save state to session
  const url = new URL("https://api.profy.cn/oauth/authorize");
  url.searchParams.set("client_id", APP_UUID);
  url.searchParams.set("redirect_uri", "https://your-app.com/callback");
  url.searchParams.set("response_type", "code");
  url.searchParams.set("scope", "events:write");
  url.searchParams.set("state", state);
  res.redirect(url.toString());
});

// 2. OAuth callback
app.get("/callback", async (req, res) => {
  const { code, state } = req.query;
  // Verify state

  const tokenResp = await fetch("https://api.profy.cn/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      grant_type: "authorization_code",
      code,
      redirect_uri: "https://your-app.com/callback",
      client_id: APP_UUID,
      client_secret: APP_SECRET,
    }),
  });
  const tokens = await tokenResp.json();
  // Save tokens to database
  res.redirect("/dashboard");
});

// 3. Use OAuth Token to call API
app.post("/api/generate", async (req, res) => {
  const userToken = getUserToken(req); // Retrieve from database
  const client = new Profy({ apiKey: userToken.access_token });

  try {
    const result = await client.chat.completions.create({
      model: "deepseek-v3",
      messages: [{ role: "user", content: req.body.prompt }],
    });
    res.json({ text: result.text });
  } catch (e) {
    res.status(500).json({ error: "Generation failed" });
  }
});

app.listen(3000);
```

## Best Practices

<AccordionGroup>
  <Accordion title="Store tokens securely">
    OAuth Tokens should be encrypted and stored in your server-side database. Do not expose them on the frontend. Use httpOnly cookies or sessions to pass user identity.
  </Accordion>

  <Accordion title="Implement automatic token refresh">
    Access Tokens have a 1-hour validity period. When an API call returns 401, automatically use the Refresh Token to obtain a new token, making it transparent to the user.
  </Accordion>

  <Accordion title="Use idempotency keys to prevent duplicate charges">
    Idempotency keys for the Events API should be uniquely tied to the business operation (e.g., `{userId}_{taskId}`), ensuring network retries don't cause duplicate charges.
  </Accordion>

  <Accordion title="Display credit prices to users">
    Use the Meters API to retrieve feature prices and show users the credits that will be deducted before they use a paid feature. This builds trust.
  </Accordion>

  <Accordion title="Handle insufficient credits gracefully">
    When the Events API returns a billing failure, show users a recharge prompt instead of failing silently.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="OAuth Integration" icon="shield" href="/en/developers/oauth-integration">
    Complete OAuth 2.0 integration flow
  </Card>

  <Card title="Events API" icon="bolt" href="/en/developers/api/events">
    Custom billing event reporting
  </Card>
</CardGroup>
