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

# Building Your App Logic

> Use Cloud APIs to read data, perform actions, and create alerts from your Custom App.

export const CtaCallout = props => {
  const {title, buttonLabel, href, trackingEvent, buttonTarget, rel = "noopener noreferrer", children} = props;
  const handleCtaClick = () => {
    if (typeof window === "undefined" || !trackingEvent) {
      return;
    }
    try {
      window.dispatchEvent(new CustomEvent("blnk:docs-cta", {
        detail: {
          name: trackingEvent,
          href
        }
      }));
    } catch {}
    try {
      window.posthog?.capture?.(trackingEvent, {
        href
      });
    } catch {}
    const gaPayload = {
      cta_href: href
    };
    try {
      window.gtag?.("event", trackingEvent, gaPayload);
    } catch {}
    try {
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push({
        event: trackingEvent,
        ...gaPayload
      });
    } catch {}
  };
  const isExternal = typeof href === "string" && (/^https?:\/\//i).test(href);
  const target = buttonTarget ?? (isExternal ? "_blank" : undefined);
  const linkRel = isExternal ? rel : undefined;
  return <section className="cta-callout not-prose relative my-8 w-full min-w-0 overflow-hidden rounded-xl border border-zinc-200 p-5 dark:border-white/10">
      <div className="cta-callout-noise" aria-hidden="true" />
      <div className="cta-callout-layout">
        {title ? <div className="cta-callout-title-row">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" width="14" height="14" className="cta-callout-icon shrink-0 text-zinc-800 dark:text-zinc-200" aria-hidden="true">
              <g fill="none" fillRule="nonzero">
                <path d="M28 0v28H0V0h28ZM14.691833333333335 27.134333333333334l-0.012833333333333334 0.0023333333333333335 -0.08283333333333333 0.04083333333333334 -0.023333333333333334 0.004666666666666667 -0.016333333333333335 -0.004666666666666667 -0.08283333333333333 -0.04083333333333334c-0.011666666666666667 -0.004666666666666667 -0.022166666666666668 -0.0011666666666666668 -0.028000000000000004 0.005833333333333334l-0.004666666666666667 0.011666666666666667 -0.019833333333333335 0.49933333333333335 0.005833333333333334 0.023333333333333334 0.011666666666666667 0.015166666666666667 0.12133333333333333 0.08633333333333333 0.0175 0.004666666666666667 0.014000000000000002 -0.004666666666666667 0.12133333333333333 -0.08633333333333333 0.014000000000000002 -0.018666666666666668 0.004666666666666667 -0.019833333333333335 -0.019833333333333335 -0.4981666666666667c-0.0023333333333333335 -0.011666666666666667 -0.0105 -0.019833333333333335 -0.019833333333333335 -0.021Zm0.3091666666666667 -0.13183333333333336 -0.015166666666666667 0.0023333333333333335 -0.21583333333333335 0.1085 -0.011666666666666667 0.011666666666666667 -0.0035000000000000005 0.012833333333333334 0.021 0.5016666666666667 0.005833333333333334 0.014000000000000002 0.009333333333333334 0.008166666666666668 0.23450000000000004 0.1085c0.014000000000000002 0.004666666666666667 0.026833333333333334 0 0.03383333333333334 -0.009333333333333334l0.004666666666666667 -0.016333333333333335 -0.03966666666666667 -0.7163333333333334c-0.0035000000000000005 -0.014000000000000002 -0.011666666666666667 -0.023333333333333334 -0.023333333333333334 -0.025666666666666667Zm-0.8341666666666667 0.0023333333333333335a0.026833333333333334 0.026833333333334334 0 0 0 -0.0315 0.007000000000000001l-0.007000000000000001 0.016333333333333335 -0.03966666666666667 0.7163333333333334c0 0.014000000000000002 0.008166666666666668 0.023333333333333334 0.019833333333333335 0.028000000000000004l0.0175 -0.0023333333333333335 0.23450000000000004 -0.1085 0.011666666666666667 -0.009333333333333334 0.004666666666666667 -0.012833333333333334 0.019833333333333335 -0.5016666666666667 -0.0035000000000000005 -0.014000000000000002 -0.011666666666666667 -0.011666666666666667 -0.21466666666666667 -0.10733333333333334Z" strokeWidth="1.1667" />
                <path fill="currentColor" d="M14 2.916666666666667A1.75 1.75 0 0 1 15.750000000000002 4.666666666666667v6.302333333333334L21.207666666666668 7.816666666666667a1.75 1.75 0 0 1 1.75 3.031L17.5 14l5.457666666666667 3.151166666666667a1.75 1.75 0 0 1 -1.75 3.031l-5.457666666666667 -3.1500000000000004V23.333333333333336a1.75 1.75 0 0 1 -3.5 0v-6.302333333333334L6.792333333333334 20.183333333333337a1.75 1.75 0 1 1 -1.75 -3.031L10.5 14 5.042333333333334 10.848833333333333a1.75 1.75 0 0 1 1.75 -3.031l5.457666666666667 3.1500000000000004V4.666666666666667A1.75 1.75 0 0 1 14 2.916666666666667Z" strokeWidth="1.1667" />
              </g>
            </svg>
            <p className="cta-callout-title min-w-0 font-semibold text-zinc-800 dark:text-zinc-200">
              {title}
            </p>
          </div> : null}
        <div className={`cta-callout-body text-sm leading-normal text-zinc-800 dark:text-zinc-200${title ? " cta-callout-body--indented" : ""}`}>
          {children}
        </div>
        <a href={href} target={target} rel={linkRel} onClick={handleCtaClick} data-docs-cta={trackingEvent || undefined} className="cta-callout-button inline-flex items-center justify-center gap-1 rounded-full bg-white px-3 py-1.5 text-sm font-semibold transition hover:bg-zinc-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/50 dark:bg-white dark:hover:bg-zinc-200">
          {buttonLabel}
          <span className="cta-callout-button-arrow" aria-hidden="true">
            →
          </span>
        </a>
      </div>
    </section>;
};

<Note>
  This feature is in private beta. If you want access, please [contact Support](mailto:support@blnkfinance.com?subject=Interested%20in%20Custom%20Apps).
</Note>

After installation, your app now has what it needs to work with the selected Cloud instance. The important values come from the install record you saved earlier:

* `api_key`
* `instance_id`
* `granted_permissions`

Your backend should use those values to decide which instance to call, what key to use, and what actions the app is allowed to perform.

**For our Stripe Sync app,** this is where the backend starts doing the work: importing pay-ins from Stripe, sending them to Blnk as transactions, and saving the sync details locally.

***

## Choose the right Cloud API

Custom Apps usually use three classes of Cloud APIs.

| API                  | When to use it                                                  |
| -------------------- | --------------------------------------------------------------- |
| Data API             | Read and filter ledger data through Cloud.                      |
| Proxy API            | Perform Core actions through Cloud.                             |
| Other Cloud features | Use Cloud features that are not Core endpoints, such as alerts. |

All requests use the Cloud base URL:

```bash theme={"system"}
https://api.cloud.blnkfinance.com
```

The installed app API key goes in the `Authorization` header:

```bash theme={"system"}
Authorization: Bearer <api_key>
```

For Proxy and Data API requests, include the selected `instance_id` in the URL:

```bash theme={"system"}
?instance_id=<instance_id>
```

***

## Quick reference

<Tabs>
  <Tab title="Using the Proxy API">
    Use the proxy when your app wants to call Blnk Core through Cloud. The format is:

    ```bash wrap theme={"system"}
    https://api.cloud.blnkfinance.com/proxy/<core-endpoint>?instance_id=<instance_id>
    ```

    The `<core-endpoint>` is the same endpoint you would normally call on Core, but with `/proxy` in front of it.

    For example, on Core, the request would look like this:

    <CodeGroup>
      ```bash List ledgers theme={"system"}
      GET http://localhost:5001/ledgers
      ```

      ```bash Create transactions theme={"system"}
      POST http://localhost:5001/transactions
      ```
    </CodeGroup>

    With the Cloud Proxy, it becomes:

    <CodeGroup>
      ```bash List ledgers wrap theme={"system"}
      GET https://api.cloud.blnkfinance.com/proxy/ledgers?instance_id=inst_...
      ```

      ```bash Create transactions wrap theme={"system"}
      POST https://api.cloud.blnkfinance.com/proxy/transactions?instance_id=inst_...
      ```
    </CodeGroup>

    A complete request via the Cloud Proxy would look like:

    ```bash bash wrap theme={"system"}
    curl -X GET "https://api.cloud.blnkfinance.com/proxy/ledgers?instance_id=inst_..." \
      -H "Authorization: Bearer blnk_..." \
      -H "Content-Type: application/json"
    ```

    <Card title="Using the proxy" icon="send" href="/cloud/proxy/proxy-api">
      Proxy endpoints and request shapes.
    </Card>
  </Tab>

  <Tab title="Using the Data API">
    Use the Data API when your app needs to read or filter ledger data through Cloud.

    The format is:

    ```bash wrap theme={"system"}
    https://api.cloud.blnkfinance.com/data/<resource>?instance_id=<instance_id>
    ```

    For example, to read transactions:

    ```bash wrap theme={"system"}
    curl -X GET "https://api.cloud.blnkfinance.com/data/transactions?instance_id=inst_..." \
      -H "Authorization: Bearer blnk_..." \
      -H "Content-Type: application/json"
    ```

    You can also add filters to the query string:

    ```bash wrap theme={"system"}
    curl -X GET "https://api.cloud.blnkfinance.com/data/transactions?instance_id=inst_...&status_eq=APPLIED&currency_eq=USD" \
      -H "Authorization: Bearer blnk_..." \
      -H "Content-Type: application/json"
    ```

    <Card title="Using the data API" icon="database" href="/cloud/proxy/data-api">
      Data API operations and read patterns.
    </Card>
  </Tab>

  <Tab title="Other Cloud features">
    Use other Cloud endpoints for features that belong to Cloud itself.

    Alerts are a good example. You do not call alerts through `/proxy` or `/data`. You call the Alerts API directly.

    ```bash wrap theme={"system"}
    https://api.cloud.blnkfinance.com/alerts/flag/<resource_id>
    ```
  </Tab>
</Tabs>

***

## Example application

Let's apply this to build the [Stripe Sync workflow](/cloud/apps/define-workflow#map-your-workflow) we mapped earlier.

<Steps>
  <Step title="Fetch pay-ins from Stripe">
    First, we'll list pay-ins from Stripe and keep only the ones that have actually settled (`status === "succeeded"`):

    ```typescript fetchStripePayIns.ts wrap theme={"system"}
    import Stripe from "stripe";

    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

    async function fetchStripePayIns() {
      const paymentIntents = await stripe.paymentIntents.list({ limit: 100 });

      return paymentIntents.data
        .filter((pi) => pi.status === "succeeded")
        .map((pi) => ({
          id: pi.id,
          amount: pi.amount_received,
          currency: pi.currency.toUpperCase(),
          customer: pi.customer as string,
          created_at: new Date(pi.created * 1000).toISOString(),
        }));
    }
    ```

    Filtering on `status === "succeeded"` ensures we only import pay-ins that have actually settled into your Stripe balance - anything still pending, requiring action, or canceled is skipped.
  </Step>

  <Step title="Send the pay-in to Blnk">
    Next, we'll mirror the Stripe pay-in as a transaction in the selected Blnk Cloud instance through the Proxy API:

    ```typescript sendPayInToBlnk.ts wrap theme={"system"}
    async function sendPayInToBlnk(instance_id: string, payIn) {
      const headers = {
        Authorization: `Bearer ${api_key}`,
        "Content-Type": "application/json",
      };

      const response = await axios.post(
        `https://api.cloud.blnkfinance.com/proxy/transactions?instance_id=${instance_id}`,
        {
          precise_amount: payIn.amount,
          precision: 100,
          reference: payIn.id,
          currency: payIn.currency,
          source: "@stripe",
          destination: `@${payIn.customer}`,
          description: "Imported pay-in from Stripe",
          effective_date: payIn.created_at,
          meta_data: {
            stripe_payment_intent_id: payIn.id,
            stripe_customer_id: payIn.customer,
          },
        },
        { headers }
      );

      return response.data;
    }
    ```

    We'll use the Stripe payment intent ID directly as the `reference` in Blnk. Re-syncing the same payment intent will not create a duplicate transaction in Blnk because Blnk handles idempotency internally.
  </Step>

  <Step title="Save the sync details in your app">
    Finally, we'll record the sync run in the app's local database so we know when the sync ran, how many pay-ins it processed, and whether it succeeded:

    ```typescript saveSyncRecord.ts wrap theme={"system"}
    async function saveSyncRecord(payIns, started_at) {
      await db.run(
        `INSERT INTO stripe_syncs (
          sync_id,
          records_found,
          started_at,
          completed_at
        ) VALUES (?, ?, ?, ?, ?)`,
        [
          crypto.randomUUID(),
          payIns.length,
          started_at,
          new Date().toISOString(),
        ]
      );
    }
    ```

    Call this once per sync run (after Step 2 completes for the whole batch) so each row represents one end-to-end sync, not each individual pay-in. The link back to specific Blnk transactions is already preserved by the `meta_data.stripe_payment_intent_id` written in Step 2.
  </Step>
</Steps>

***

<Card title="Run the example Stripe Sync app" icon="github" href="https://github.com/blnkfinance/apps-demo">
  Reference Stripe sync implementation.
</Card>

***

<Tip>
  Before you ship, review [Best practices](/cloud/apps/best-practices) for securing API keys, portal embedding, and permissions.
</Tip>

***

<CtaCallout title="Need help building your app?" href="https://blnkfinance.com/contact/us?utm_source=blnk_docs&utm_medium=documentation&utm_campaign=apps-help" buttonLabel="Get Pro Support" trackingEvent="clicked_pro_support">
  We help you build custom apps for your use case or get help building your own from scratch.
</CtaCallout>
