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

# Lookup payment protocol

> The endpoint every Lookup payment Provider calls, as ClassAtlas implements it: request checks in order, IP allow-list, HASH_CODE signing, debt, verify, pay and ping calls, and status codes.

A Provider calls one address per school with an HTTPS `GET`. ClassAtlas always answers with XML and HTTP 200, and the outcome is the `code` of the `status` element. The protocol began as iPay's specification (`payment-gateway.doc`, version 1.03) and is now the one every Provider uses (ADR 0074). This page records how ClassAtlas implements it.

## Endpoint

| Item | Value |
| - | - |
| Address | The school's own subdomain, with one path per Provider: `https://{school}.classatlas.ge/ipay` for iPay, `https://{school}.classatlas.ge/tbcpay` for TBC Pay |
| Method | `GET`, parameters in the query string |
| Response | XML, `Content-Type: application/xml; charset=UTF-8`, always HTTP 200 |
| Session | None: no cookies, session or CSRF token |
| Rate limit | 300 requests per minute per school |

The examples on this page use iPay's address. TBC Pay sends exactly the same requests to its own.

> [!NOTE]
>
> **Planned** ([CA-1147](https://bakidevelopment.atlassian.net/browse/CA-1147)): TBC Pay's address does not answer yet.

## Checks, in order

Every request passes these checks in this order. The first one that fails decides the answer.

| # | Check | Code on failure |
| - | - | - |
| 1 | The caller's IP address is on the allow-list | 1 |
| 2 | The subdomain belongs to a school | 1 |
| 3 | The school is under its rate limit | 99, with `retry="true"` |
| 4 | The school has this Provider switched on and fully configured | 1 |
| 5 | `USERNAME` and `PASSWORD` match the school's | 2 |
| 6 | `HASH_CODE` matches | 3 |
| 7 | The parameters are valid for the operation | 4, 10, 7 or 5 |
| 8 | The operation itself succeeds | 6, 9, 8 or 99 |

The IP check runs before the school is resolved, so a stranger never opens a school's database or uses up its rate limit. Authentication runs after, because each school's credentials live in its own database (ADR 0029).

> [!NOTE]
>
> Any unexpected error on this route is answered with code 99, never with an HTML error page.

## Authentication

### IP allow-list

Only the Providers' servers are accepted. A Provider gives ClassAtlas the addresses it calls from, and one list serves every school and every Provider.

* The check uses the address the connection came from, not `X-Forwarded-For`, which any caller can forge (ADR 0028).
* An address on the list still needs that Provider's credentials and secret key at the school.

### Credentials

Each school has its own `USERNAME`, `PASSWORD` and secret key at each Provider, stored encrypted in the school's database. The username and the password are both compared before answering, in constant time, so a wrong username and a wrong password take the same time.

### HASH\_CODE

`HASH_CODE` is the MD5 of every parameter value, in the order the parameters arrive, followed by the school's secret key, in uppercase hex.

```text theme={null}
HASH_CODE = UPPER( MD5( v1 + v2 + … + vN + SECRET_KEY ) )
```

* `HASH_CODE` itself is left out, wherever it appears in the request.
* Values are taken URL-decoded and joined with no separator.
* The order is the order of arrival, not a fixed list. If a Provider adds a parameter, the hash still validates (ADR 0026).

Example for `pay`, with test values and the secret key `DemoSecretKey123`:

```text theme={null}
MD5 input:  payipay_demoDemo2026passtuition_fee0100101234525000mbankTEST0000000001DemoSecretKey123
HASH_CODE:  BA3DE786E37DDEDBC642763F251E36A8
```

> [!NOTE]
>
> Whether a Provider hashes the decoded or the raw value is unconfirmed. It only matters for `EXTRA_INFO`, the one parameter that could carry characters needing encoding, and iPay has confirmed it sends nothing there. A Provider's test run against a school before go-live settles it.

## Parameters

A parameter is required in the operations listed, unless marked optional.

| Parameter | Operations | Description |
| - | - | - |
| `OP` | All | `debt`, `verify`, `pay` or `ping` |
| `USERNAME` | All | The school's username |
| `PASSWORD` | All | The school's password |
| `SERVICE_ID` | `debt`, `verify`, `pay` | One of the school's services. See [Services, settlement accounts and student lookup](https://docs.classatlas.ge/en/payments/lookup-payments/services-and-students.md) |
| `CUSTOMER_ID` | `debt`, `verify`, `pay` | The student's personal number |
| `PAY_AMOUNT` | `verify`, `pay` | Amount in tetri, a whole number from 1 to 99,999,999 (999,999.99 GEL) |
| `PAY_SRC` | `pay`, optional | The channel. See [Channels](https://docs.classatlas.ge/en/payments/lookup-payments/overview.md#channels) |
| `PAYMENT_ID` | `pay` | The Provider's ID for the payment, up to 255 characters |
| `EXTRA_INFO` | `pay`, optional | Kept with the transaction as evidence. iPay sends nothing in it |
| `HASH_CODE` | All | The signature |

The upper limit on `PAY_AMOUNT` is the largest amount a transaction can hold.

## Responses

Every response is one `pay-response` document: a `status` with a code and a text, a `timestamp` in Unix seconds, and the operation's result when it succeeds. A refusal also carries `retry`.

```xml Refusal theme={null}
<?xml version="1.0" encoding="UTF-8"?>
<pay-response>
  <status code="6" retry="false">Customer does not exist</status>
  <timestamp>1793704705</timestamp>
</pay-response>
```

`retry` is `false` except when the school's rate limit was hit. Telling a Provider a refused payment may be retried when it may not is how a double payment happens.

## Operations

### debt

Returns what the student owes and what it is made of.

```text Request theme={null}
https://demo.classatlas.ge/ipay?OP=debt&USERNAME=ipay_demo&PASSWORD=Demo2026pass&SERVICE_ID=tuition_fee&CUSTOMER_ID=01001012345&HASH_CODE=43530F79CCA1BACE518C41CD4CEAD4CE
```

```xml Response theme={null}
<?xml version="1.0" encoding="UTF-8"?>
<pay-response>
  <status code="0">OK</status>
  <timestamp>1793704705</timestamp>
  <debt>25000</debt>
  <additional-info>
    <parameter name="first_name">ნინო</parameter>
    <parameter name="last_name">ბერიძე</parameter>
    <parameter name="charge_1">ოქტ. 2026 — I ჯგუფი — 150.00 ₾</parameter>
    <parameter name="charge_2">ნოე. 2026 — I ჯგუფი — 150.00 ₾</parameter>
    <parameter name="balance">ბალანსი — 50.00 ₾</parameter>
  </additional-info>
</pay-response>
```

`debt` is the student's charges less their balance, never below zero, in tetri. It is read from the same balance view the ClassAtlas panel uses, so the bank and the school always see the same number.

`additional-info` is what the payer reads. The values carry the whole line, and the names are only keys, because the specification does not say whether a Provider displays the names. The text is always in Georgian, whatever the application's language.

| `name` | Sent when | Value |
| - | - | - |
| `first_name`, `last_name` | Always | The student's first and last name |
| `charge_1` … `charge_N` | Once per unpaid charge, oldest first | The charge's title, then the amount: `ოქტ. 2026 — I ჯგუფი — 150.00 ₾`. A monthly charge's title is the short month, the year and the group; an invoice keeps its own title |
| `balance` | The school holds credit for the student | ბალანსი — amount ₾ |

While the student owes, `balance` is the credit already counted against the charges, so the charges less the balance equal `debt`. When they owe nothing it is their credit, so a parent can tell "we are square" from "the school does not know my child".

### verify

Answers whether a payment may go ahead. It records nothing, because in the teller flow it is sent before the customer has handed over any money.

It refuses only a student who has left the school and owes nothing (code 9). There is no upper limit on the amount: a parent may pay more than they owe.

```xml Response theme={null}
<?xml version="1.0" encoding="UTF-8"?>
<pay-response>
  <status code="0">OK</status>
  <timestamp>1793704705</timestamp>
</pay-response>
```

### pay

Records the payment and returns its receipt number.

```text Request theme={null}
https://demo.classatlas.ge/ipay?OP=pay&USERNAME=ipay_demo&PASSWORD=Demo2026pass&SERVICE_ID=tuition_fee&CUSTOMER_ID=01001012345&PAY_AMOUNT=25000&PAY_SRC=mbank&PAYMENT_ID=TEST0000000001&HASH_CODE=BA3DE786E37DDEDBC642763F251E36A8
```

```xml Response theme={null}
<?xml version="1.0" encoding="UTF-8"?>
<pay-response>
  <status code="0">OK</status>
  <timestamp>1793704705</timestamp>
  <receipt-id>48213</receipt-id>
</pay-response>
```

1. The `verify` check runs again, because a teller's order can be paid long after it was created.
2. One transaction is written for the student. `receipt-id` is its ID in ClassAtlas.
3. Everything else, from paying off charges to messaging the family, follows from that transaction. See [How ClassAtlas records a Lookup payment](https://docs.classatlas.ge/en/payments/lookup-payments/recording-payments.md).

A `PAYMENT_ID` is recorded once per Provider and channel. The same ID arriving again on the same channel is answered with code 8 and records nothing. A retry after a lost response therefore gets code 8, not the original `receipt-id`. iPay has confirmed that codes 8 and 18 mean the same thing to them.

There is no operation to cancel a payment.

### ping

Answers whether the school can take payments right now. It passes every authentication check, then reads the student balance view that every money operation depends on.

```text Request theme={null}
https://demo.classatlas.ge/ipay?OP=ping&USERNAME=ipay_demo&PASSWORD=Demo2026pass&HASH_CODE=AFAA13AAD6730A86D47EDB59D6882CD1
```

A broken database answers code 99, which the Provider alerts on. A paused or unconfigured school answers code 1.

## Status codes

| Code | Text | ClassAtlas answers it when |
| - | - | - |
| 0 | OK | The operation succeeded |
| 1 | Access is restricted | The IP address is not allowed, the subdomain has no school, or the Provider is off or incomplete for the school |
| 2 | Username or password is invalid | The credentials do not match the school's |
| 3 | Hash code is invalid | `HASH_CODE` does not match |
| 4 | Required parameter is missing | A required parameter is absent. This wins over every other parameter error |
| 5 | Parameter value is invalid | A value cannot be read, for example an unknown `OP` or an amount that is not a whole number |
| 6 | Customer does not exist | No student with that personal number at this service, or a student who left owing nothing |
| 7 | Payment amount is invalid | The amount is below 1 tetri or above 999,999.99 GEL |
| 8 | Payment id is not unique | This `PAYMENT_ID` is already recorded for this channel |
| 9 | Payment is not possible | The student left the school and owes nothing |
| 10 | Service does not exist | `SERVICE_ID` is not one of the school's services |
| 99 | General error | The rate limit was hit (`retry="true"`), or an unexpected failure (`retry="false"`) |

Parameter errors are reported in a fixed order: a missing parameter (4), then an unknown service (10), then an amount out of range (7), then any other bad value (5). Code 18 exists in the specification, but ClassAtlas never sends it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.