> For the complete documentation index, see [llms.txt](https://apc-pay.gitbook.io/apc-pay-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://apc-pay.gitbook.io/apc-pay-docs/apc-pay/authentication-and-request-signing.md).

# Authentication and Request Signing

Updated 2026-10-14

Every APC Pay API request is signed with your merchant private key. Successful response data and payout callbacks are signed by APC Pay and must be verified with the platform public key.

## Trust model

| Direction                    | Signing key          | Verification key                               |
| ---------------------------- | -------------------- | ---------------------------------------------- |
| Merchant request to APC Pay  | Merchant private key | Merchant public key configured for the app     |
| APC Pay response to merchant | Platform private key | Platform public key downloaded from the portal |
| APC Pay callback to merchant | Platform private key | Platform public key downloaded from the portal |

Keep private keys server-side. Pin the platform public key from a trusted source and verify its fingerprint before use.

## Common request fields

Every request body is JSON and includes its operation-specific fields plus:

| Field      |   Type | Required | Description                                                                                                   |
| ---------- | -----: | :------: | ------------------------------------------------------------------------------------------------------------- |
| `mchNo`    | string |    Yes   | Merchant number.                                                                                              |
| `appId`    | string |    Yes   | App ID.                                                                                                       |
| `reqTime`  | string |    Yes   | UTC RFC 3339 timestamp with milliseconds, within 5 minutes of platform time.                                  |
| `nonceStr` | string |    Yes   | Random value, recommended length 32 characters. It must be unique for the app during the request-time window. |
| `sign`     | string |    Yes   | Standard Base64 RSA-SHA256 signature.                                                                         |

Generate a fresh `reqTime`, `nonceStr`, and signature for every attempt, including retries.

## Build the request canonical string

1. Start with every top-level JSON field.
2. Remove `sign`.
3. Remove fields whose value is `null`, `undefined`, or an empty string.
4. Sort the remaining field names in ASCII order.
5. Render each field as `key=value`.
6. Serialize object and array values as compact JSON first. Sort keys inside objects and do not escape non-ASCII characters.
7. Percent-encode each value according to RFC 3986. Preserve only `A-Z`, `a-z`, `0-9`, `-`, `.`, `_`, and `~`; use uppercase `%XX` for all other bytes.
8. Join the fields with `&`.
9. Percent-encode the backend path using the same rule and append `uri=<encoded-backend-path>` as the final field.

For a request sent to:

```
https://pwc-pay-api.chiachair.com/v1/payout/create
```

the signed backend path is `/payout/create`, so the canonical string ends with:

```
uri=%2Fpayout%2Fcreate
```

> 📌
>
> ### Do not sign the public version prefix
>
> The supplied TypeScript implementation signs the backend path without `/v1`. If APC Pay supplies a different base URL, change the base URL only unless APC Pay confirms that the backend path also changed.

### Canonical string example

For an illustrative payout request, the canonical string has this shape:

```
appId=your-app-id&extParam=customer-withdrawal-8675309&fiatAmount=100.00&fiatCurrency=PHP&mchNo=your-merchant-number&mchOrderNo=PO-20261014-0001&nonceStr=0123456789abcdef0123456789abcdef&notifyUrl=https%3A%2F%2Fmerchant.example.com%2Fwebhooks%2Fapc-pay&qrContent=complete-payee-qr-payload&reqTime=2026-10-14T08%3A30%3A00.000Z&userEmail=payee%40example.com&uri=%2Fpayout%2Fcreate
```

## Sign the request

Sign the UTF-8 bytes of the canonical string with:

| Setting         | Value           |
| --------------- | --------------- |
| Algorithm       | SHA256withRSA   |
| RSA padding     | PKCS#1 v1.5     |
| Output encoding | Standard Base64 |

Add the Base64 result to the JSON request as `sign`.

## Verify successful API responses

Successful API responses with non-empty `data` include a platform `sign` value. Before using the response:

1. Build a canonical string from the top-level fields inside `data`.
2. Do **not** append `uri`.
3. Verify `sign` using the platform public key.
4. Require `data.nonceStr` to equal the nonce sent in your request.
5. When applicable, require the returned `mchOrderNo` or `payoutOrderId` to match the request.

Reject a successful response when `data` is empty, its signature is missing or invalid, or the nonce does not match.

> 📘
>
> ### Signed response scope
>
> The response signature authenticates `data` only. The outer `code`, `msg`, and `requestId` fields are not part of the signed canonical string.

## Verify callbacks

Callbacks use the same platform signature scheme, with two differences:

* Build the canonical string from all callback fields except `sign`.
* Do not append a `uri` field.

Verify the signature before reading or applying any callback state. Continue with [Payout Webhooks](broken://pages/3ddd4c5270f1825eb62c85633eddb69b50a33b8c) for the complete validation order.

## Common signature failures

| Symptom                            | Check                                                                                                                   |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `1003 REQUEST_EXPIRED`             | Server clock, 5-minute request window, and fresh nonce.                                                                 |
| `1004 SIGN_ERROR`                  | ASCII field ordering, omitted empty values, RFC 3986 encoding, signed path, key pair, RSA padding, and standard Base64. |
| Valid signature but wrong response | Response nonce and order identifiers match the originating request.                                                     |
| Callback signature failure         | The exact callback fields were canonicalized without `sign` and without `uri`.                                          |

Do not log private keys, full QR payloads, payee data, or replayable signed request bodies.

## Next step

Review [Balance and Currency](broken://pages/c374e00afa6fb109644d34b6d82fbe7b0d8b38c7), then continue to [Create a Payout](broken://pages/85bb23f0c564b16167e4d27ad120123daf5f90b3).
