# Take Alipay payments with an integration | Creght

> Take Alipay payments on Creght: ctx.payment.alipay.pageUrl starts a PC website payment, verifyNotify verifies the async notification, call reaches the query and refund APIs, and channel tags separate multiple receiving accounts.

[![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)API for AI](/)

[View llms.txt](/llms.txt)

Overview

- [Creght API for AI](/api.md)

Discoverability

- [How to optimize llms.txt](/api/optimize-llms-txt.md)

Site configuration

- [Implement domain-based locale routing](/api/domain-locale-routing.md)

Backend

- [Calling external APIs on the server and managing the cache](/api/ssr-external-api-cache.md)
- [Build site backend workflows with Func](/api/func-backend.md)
- [JSON tables: definition, reads, and queries](/api/func-json-tables.md)
- [Uploads: signed direct upload and Func-generated files](/api/func-assets-upload.md)
- [Timeouts and streaming responses](/api/func-timeout-streaming.md)
- [Integrate Alipay PC Web Payment with Func](/api/func-alipay-payment.md)

Integrations

- [Send Email and Verification Codes with Integrations](/api/func-email-integration.md)
- [Take Alipay payments with an integration](/api/func-alipay-integration.md)

Auth

- [Require a Verified Email to Sign Up](/api/auth-verified-registration.md)
- [Reset and Change Passwords](/api/auth-password-reset.md)
- [Sign In From a Func](/api/auth-func-login.md)
- [Query users from a Func](/api/func-user-directory.md)

On this page

- [payment is a namespace, not a unified interface](#namespace)
- [Connect Alipay](#connect)
- [What the save-time check covers](#verify)
- [The order table](#order-table)
- [Start a payment](#create)
- [Receive the async notification](#notify)
- [Query, refund and other APIs](#call)
- [Several receiving accounts in one project](#channels)
- [What the platform will not do for you](#yours)
- [Boundaries](#errors)

Integrations/Take Alipay payments with an integration

# Take Alipay payments with an integration

Connect the Alipay integration, then use ctx.payment.alipay in Func to start PC website payments and verify async notifications: the app private key stays on the server, while orders, amount checks and idempotent fulfilment stay in your own code.

Copy Markdown link

Alipay is a managed integration: connect it once in the editor, then use `ctx.payment.alipay` in Func to start PC website payments and verify async notifications. The app private key and the AES key stay on the server — signing, verification and content encryption all happen there, so no secret ever appears in your code.

**Scope**

Alipay **PC website payment** ( `alipay.trade.page.pay`) in public-key mode. WAP payment, in-app payment, face-to-face payment and certificate mode are not supported yet; you can still implement those in Func yourself — see [Alipay PC website payment with Func](/api/func-alipay-payment.md).

## payment is a namespace, not a unified interface

Method names follow Alipay's own product shape. There is deliberately **no cross-provider payment abstraction**: parameter structures and callback semantics differ far too much (Stripe has Checkout Sessions, Alipay has signed form redirects), so a forced `createCheckout()` would only be a harder-to-read forwarder. Adding another provider later means adding `ctx.payment.<provider>`; existing code is untouched.

The split: the platform owns **cryptography and credentials**, your code owns **money and goods**.

| Guaranteed by the platform | Still yours to write |
| --- | --- |
| RSA2 signing, and notification verification over the raw body | Amounts come from a server-side product table |
| Checking `app_id` / `seller_id` belong to this channel | The order table and its state |
| Optional AES content encryption | Idempotent fulfilment |
| Verifying OpenAPI responses against the raw response text | Comparing the amount with your own order |
| A real credential check when you save | — |

## Connect Alipay

Collect the parameters in the Alipay console first:

1. Open the app that receives the money and note its `APPID`.
2. Under 开发设置 → 接口加签方式, generate an RSA2 key pair with the [Alipay developer tool](https://open.alipay.com/tool): **keep the app private key**, upload only the app public key, then copy the **Alipay public key** (支付宝公钥) shown in the same place.
3. Note the Alipay user id (PID) of the receiving account; it must match `seller_id` on notifications.
4. If you enabled 接口内容加密方式, copy that base64 AES key; skip it otherwise.

Then open **Backend → Integrations** in the editor, pick **Alipay** and fill the table below. Saving calls Alipay for real, so a wrong value fails immediately instead of when your first customer pays.

| Field | Value | Required |
| --- | --- | --- |
| APPID | The APPID of your Alipay app | Yes |
| App private key | The private key you generated and keep — **not** the app public key. PKCS#8 or PKCS#1, with or without PEM headers | Yes |
| Alipay public key | The key **Alipay gives you** after you upload your app public key | Yes |
| Seller PID | Alipay user id of the receiving account | Yes |
| Async notification URL | A published https Func URL, e.g. `https://example.com/func/alipay.notify` | Yes |
| Return URL | Where the payer lands after paying; page experience only | No |
| AES key | The base64 string from 接口内容加密方式 | Only with content encryption |
| Gateway | Blank means production; sandbox needs its own gateway | Sandbox only |

**The Alipay public key is the field people get wrong**

Your app public key is what you upload to Alipay; the Alipay public key is what Alipay hands back. They are not interchangeable, and swapping them makes saving fail at the response-verification step.

## What the save-time check covers

Saving runs `alipay.trade.query` against a random order id. That single call validates four things at once: the `APPID` exists and the interface is enabled; the app private key and the uploaded app public key are **the same pair** (Alipay accepts the signature); the Alipay public key is correct (it verifies Alipay's response signature); and the gateway matches the APPID (production vs. sandbox). With an AES key configured, that is checked too.

**"Trade does not exist" is the success signal** — the order id is deliberately one that cannot exist, so reaching that business error proves the request passed signing and permission checks.

| Error | What to fix |
| --- | --- |
| `isv.invalid-signature` | The app private key does not match the app public key uploaded to Alipay |
| `isv.invalid-app-id` | Wrong APPID, or a sandbox APPID paired with the production gateway |
| Response signature failed | Wrong Alipay public key (usually your own app public key by mistake) |
| `isv.decrypt-error` | Wrong AES key |
| `isv.insufficient-isv-permissions` | The app has not signed up for the product; enable it in the Alipay console |

## The order table

Payment state lives in your own table. Create `/platform/table/payment_orders.json`:

```json
{
  "name": "Payment orders",
  "desc": "Alipay order state",
  "json_schema": {
    "type": "object",
    "properties": {
      "userId": { "type": "string" },
      "productId": { "type": "string" },
      "amount": { "type": "string" },
      "outTradeNo": { "type": "string" },
      "status": { "type": "string", "enum": ["pending", "paid", "closed"] },
      "alipayTradeNo": { "type": "string" },
      "paidAt": { "type": "string" }
    },
    "required": ["userId", "productId", "amount", "outTradeNo", "status"]
  }
}
```

## Start a payment

Create `/backend/func/alipay.ts`:

```typescript
import type { TalizenFuncContext } from 'talizen/func-runtime'

// The amount comes from a server-side product table; the browser only sends productId.
const PRODUCTS = {
  starter: { subject: 'Starter plan', amount: '9.90' },
} as const

export function create(input: { productId?: string }, ctx: TalizenFuncContext) {
  const user = ctx.auth.requireUser()
  const productId = String(input.productId || '') as keyof typeof PRODUCTS
  const product = PRODUCTS[productId]
  if (!product) throw new Error('invalid product')

  // Generate the order id yourself and store the row first: it is both the
  // primary key of your order table and the idempotency key of the notification.
  const outTradeNo = 'C' + crypto.randomUUID().replace(/-/g, '')
  ctx.db.insert('payment_orders', {
    userId: String(user.id),
    productId,
    amount: product.amount,
    outTradeNo,
    status: 'pending',
  })

  const { payUrl } = ctx.payment.alipay.pageUrl({
    outTradeNo,
    subject: product.subject,
    amount: product.amount,
  })
  return { outTradeNo, payUrl }
}
```

`pageUrl` returns `{ payUrl, outTradeNo, amount, appId }`; send the browser to `payUrl`:

```typescript
import { invoke } from 'talizen/func'

const { payUrl } = await invoke<{ payUrl: string }>('alipay.create', {
  productId: 'starter',
})
window.location.assign(payUrl)
```

| Parameter | Notes |
| --- | --- |
| `outTradeNo` | Required, up to 64 printable ASCII chars. **The platform does not generate it**: it is your order table's key and your idempotency key, so the row must exist before you redirect |
| `subject` | Required, the product title |
| `amount` | Required, in yuan with at most 2 decimals ( `9.9` is normalised to `9.90`) |
| `body` | Optional description |
| `returnUrl` | Optional, overrides the configured return URL (page experience only) |

There is deliberately **no** `notifyUrl` override: payment facts must arrive through exactly one door, so the notification URL is configured once in the integration.

## Receive the async notification

The notification URL you configured points at this method — `https://example.com/func/alipay.notify` maps to `notify` in `/backend/func/alipay.ts`:

```typescript
export async function notify(_input: unknown, ctx: TalizenFuncContext) {
  // Verification failures throw, so a forged notification never reaches the lines below.
  // Do not catch it into a success: anything but 'success' makes Alipay retry.
  const n = ctx.payment.alipay.verifyNotify(await ctx.request.text())
  if (!n.paid) return new Response('success') // closed or other states

  const { list } = ctx.db.query('payment_orders', {
    where: { outTradeNo: n.outTradeNo },
    limit: 1,
  })
  const order = list[0]
  if (!order) return new Response('failure', { status: 400 })
  if (order.amount !== n.totalAmount) return new Response('failure', { status: 400 })
  if (order.status === 'paid') return new Response('success') // duplicate notification

  ctx.db.update('payment_orders', order.id, {
    status: 'paid',
    alipayTradeNo: n.tradeNo,
    paidAt: new Date().toISOString(),
  })
  // Granting entitlements must be idempotent on outTradeNo / tradeNo too.
  return new Response('success')
}
```

`verifyNotify` must receive the **raw form body** ( `await ctx.request.text()`). Never parse and re-serialise it: the signature covers the raw text, and rebuilding it breaks verification.

The platform does the RSA2 verification, checks that `app_id` and `seller_id` belong to this channel, and checks the required fields. **Any failure throws** rather than returning something you could mistake for falsy.

| Field | Notes |
| --- | --- |
| `paid` | Covers both `TRADE_SUCCESS` and `TRADE_FINISHED` — use it instead of comparing strings yourself |
| `outTradeNo` / `tradeNo` | Your order id / Alipay's trade id |
| `totalAmount` | Amount paid; compare it with your own order |
| `tradeStatus` | The raw trade status |
| `notifyId` | Notification id, useful for de-duplication |
| `buyerId` / `gmtPayment` / `subject` | Buyer id, payment time, title |
| `params` | Every raw parameter; read `passback_params` and friends here |

**You return success, and you own idempotency**

The Func must return the plain text `success` Alipay expects. Anything else (including an error) makes Alipay retry, so when the same `outTradeNo` arrives twice, just return `success`.

## Query, refund and other APIs

Use `call` for the remaining OpenAPI methods; the platform signs the request and verifies the response against its raw text:

```typescript
// Query: for reconciliation, or to confirm once when the payer returns
const r = ctx.payment.alipay.call('alipay.trade.query', { out_trade_no: 'C0001' })
if (r.trade_status === 'TRADE_SUCCESS') { /* ... */ }

// Refund
ctx.payment.alipay.call('alipay.trade.refund', {
  out_trade_no: 'C0001',
  refund_amount: '9.90',
  out_request_no: 'R0001', // refund request id; repeated refunds are idempotent on it
})
```

You get the verified business node (the contents of `alipay_xxx_response`); a business code other than `10000` throws. `alipay.trade.page.pay` cannot go through `call` — it is a redirect flow, so use `pageUrl`.

## Several receiving accounts in one project

Add two Alipay integrations, give them different **channel tags**, and pick one with `via()`:

```typescript
ctx.payment.alipay.pageUrl({ ... })                   // default channel, same as via('default')
ctx.payment.alipay.via('overseas').pageUrl({ ... })   // the other receiving account
ctx.payment.alipay.via('overseas').verifyNotify(raw)  // notifications use the same channel
```

**One deliberate difference from ctx.email**

When a tag matches several payment integrations the call **fails** instead of picking one at random. A payment is a chain across requests — an order signed with account A only ever gets notifications carrying A's `app_id`, so picking at random produces "the customer paid but the site never hears about it", a bug with no local symptoms.

So two receiving accounts means two tags, and the **same** tag must start the payment and handle its notification.

## What the platform will not do for you

These are where payments actually go wrong, and the platform cannot help:

- **Amounts come from a server-side product table**; the browser only sends a `productId`. Letting the browser send the amount lets it set the price.
- **Payment is only ever confirmed by a verified async notification.** The `returnUrl` is just a page the payer can open directly — never a proof of payment.
- **After verification, still check your local order and the amount**, then whether `tradeNo` matches what you recorded.
- **Fulfilment must be idempotent**, keyed by `outTradeNo` / `tradeNo` — notifications are retried.

## Boundaries

- PC website payment in public-key mode only; WAP, in-app, face-to-face and certificate mode are not done yet.
- Payment integrations **cannot** enable "expose the key to Func code": the merchant private key never enters the sandbox, the platform signs instead. To do everything yourself, take the other road — set your own `ALIPAY_*` variables under **Backend → Env** and write Func with `fetch` \+ `crypto.subtle`. The two paths do not interfere; see [Alipay PC website payment with Func](/api/func-alipay-payment.md).
- Sandbox and production use separate apps, keys and gateways. **Before going live you must receive one real notification in production** — nothing substitutes for that.
- The notification URL must be a **published** https address: Func on a preview domain will not receive live notifications.

**Func basics**

Files, invocation, JSON tables, auth, asset uploads, timeouts and SSE are covered in the [full Func backend guide](/api/func-backend.md); email and verification codes in [Send email and verification codes with an integration](/api/func-email-integration.md).

![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)

This website is built with [Creght](/)

[Discord](https://discord.gg/Qvq2nmZNnb)

## Links

- [Pricing](/price.md)
- [Solutions](/solution.md)
- [Customers](/customers.md)
- [Help Center](/help.md)
- [Contact Us](/contact.md)
- [Update Logs & Blogs](/blogs.md)
- [Refund Policy](/tuikuan.md)

## Resources

- [All Resources](/resources.md)
- [Templates](/templates.md)
- [Components](https://creghtlib.site.creght.com)
- [Animations](/design/effects.md)
- [Figma to Creght](/figma2creght.md)
- [API for AI](/api.md)

## Terms

- [Terms of Service](/legal/terms.md)
- [Privacy Policy](/legal/privacy.md)
- [Acceptable Use Policy](/legal/acceptable-use.md)

## Social Media

- [Twitter](https://twitter.com)
- [Facebook](https://www.facebook.com)
- [LinkedIn](https://www.linkedin.com)

[蜀ICP备2023038192号-2](https://beian.miit.gov.cn)
