# Backend Validation & Merchant API

Your backend validates a purchase by querying the Onside Merchant API for an order, verifying the signed response, and granting entitlements. This is a server-to-server flow — never ship your Merchant Secret in the app.

## Prerequisites

Obtain from the Onside Manager:

* a **Merchant ID**
* a **Merchant Secret**
* a **Secret Key ID** (`kid`)

## Authentication

Authenticate each request with a JSON Web Token (JWT) in the `Authorization` header, signed with **HS256** using your Merchant Secret.

**Header**

* `alg`: `HS256`
* `kid`: your Secret Key ID

**Claims**

* `aud`: `onside`
* `iss`: `onside`
* `sub`: your Merchant ID
* `exp` and `nbf`: a validity window of **no more than 30 seconds**
* `jti`: a unique token ID

## Request

Fetch the in-app purchase history for an order:

```http
GET /merchant-api/v1/purchases/in-app/history/{order_id}
Authorization: Bearer <merchant JWT>
```

## Response

On success the Merchant API returns a signed **JWS**. After verifying it, the payload is a `TransactionHistory` object listing transaction events.

```json
{
  "transactions": [
    {
      "app_account_token": "user-uuid-1234",
      "bundle_id": "com.example.app",
      "currency": "EUR",
      "expires_at": null,
      "order_id": "3e46c75a-7e04-4530-979a-e275f3a7c50c",
      "original_order_id": "3e46c75a-7e04-4530-979a-e275f3a7c50c",
      "original_purchase_date": "2025-12-26T10:00:00Z",
      "price": 499,
      "product_id": "remove_ads",
      "product_slug": "remove-ads",
      "product_type": "NON_CONSUMABLE",
      "purchase_date": "2025-12-26T10:00:00Z",
      "quantity": 1,
      "revocation_date": null,
      "revocation_reason": null,
      "fetched_at": "2026-01-26T10:05:00Z",
      "transaction_reason": "PURCHASE",
      "user_country": "DE"
    }
  ]
}
```

## Verify the response

Verify the JWS signature using Onside's public keys, available at:

```
https://onside.io/.well-known/jwks.json
```

Then confirm the decoded payload against the `TransactionHistory` schema before granting entitlements.

## Subscriptions

A subscription order is validated through the same endpoint and the same `TransactionHistory` shape. A subscription transaction differs from a one-time purchase in a few fields:

* `product_type` is `"SUBSCRIPTION"` (rather than `"CONSUMABLE"` / `"NON_CONSUMABLE"`).
* `expires_at` holds the end of the current billing period — it is `null` for non-expiring products.
* `subscription_id` identifies the subscription the transaction belongs to.
* Each renewal is an additional transaction in the `transactions` array, with `transaction_reason` `"RENEWAL"` and the same `original_order_id` as the first purchase.
* A refund sets `revocation_date` and `revocation_reason`.

Treat the subscription as **active** when its most recent transaction has an `expires_at` in the future and a `null` `revocation_date`. The example below shows an initial purchase followed by one renewal:

```json
{
  "transactions": [
    {
      "app_account_token": "user-uuid-1234",
      "bundle_id": "com.example.app",
      "currency": "EUR",
      "expires_at": "2026-02-26T10:00:00Z",
      "order_id": "3e46c75a-7e04-4530-979a-e275f3a7c50c",
      "original_order_id": "3e46c75a-7e04-4530-979a-e275f3a7c50c",
      "original_purchase_date": "2026-01-26T10:00:00Z",
      "price": 999,
      "product_id": "premium_subscription_monthly",
      "product_slug": "premium-monthly",
      "product_type": "SUBSCRIPTION",
      "purchase_date": "2026-01-26T10:00:00Z",
      "quantity": 1,
      "revocation_date": null,
      "revocation_reason": null,
      "fetched_at": "2026-02-26T10:05:00Z",
      "subscription_id": "sub-group-xyz",
      "transaction_reason": "PURCHASE",
      "user_country": "DE"
    },
    {
      "app_account_token": "user-uuid-1234",
      "bundle_id": "com.example.app",
      "currency": "EUR",
      "expires_at": "2026-03-26T10:00:00Z",
      "order_id": "9b1d2c34-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
      "original_order_id": "3e46c75a-7e04-4530-979a-e275f3a7c50c",
      "original_purchase_date": "2026-01-26T10:00:00Z",
      "price": 999,
      "product_id": "premium_subscription_monthly",
      "product_slug": "premium-monthly",
      "product_type": "SUBSCRIPTION",
      "purchase_date": "2026-02-26T10:00:00Z",
      "quantity": 1,
      "revocation_date": null,
      "revocation_reason": null,
      "fetched_at": "2026-02-26T10:05:00Z",
      "subscription_id": "sub-group-xyz",
      "transaction_reason": "RENEWAL",
      "user_country": "DE"
    }
  ]
}
```

## API reference

The full request/response schema is documented in the API reference for this section (see the navigation entry under **Backend Validation & Merchant API**).


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.onside.io/sdk/purchase-validation/merchant-api.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
