# General Requirements

> Everything cross-cutting that **every** integration must know — the core principles, the
> configuration and credentials LoyaltyPlant provisions, security, operating constraints, and a
> glossary — regardless of whether you build with the SDK or the raw API. The build guides
> ([SDK](04-sdk-integration-guide.md) / [API](05-api-integration-guide.md)) reference this page.
>
> Read after the [Overview](01-overview-and-concepts.md). This is a **two-party** setup — you
> build and host the service; LoyaltyPlant provisions the credentials and configuration that
> make it live. **You cannot self-configure** these values; LoyaltyPlant sets them for you.

---

## 1. Core principles

These five ideas run through the whole API. Each is detailed in [Payment Flows](03-payment-flows.md).

1. **Capabilities-driven.** You declare a set of *capabilities* at `GET /v1/capabilities`.
   LoyaltyPlant will **never** call an operation whose capability you didn't declare. Declare
   only what you have actually implemented and tested.
2. **Business outcome lives in HTTP 200.** A *business* result — approved, declined, pending
   — is always returned as `HTTP 200` with a `status` field. HTTP `4xx`/`5xx` are reserved for
   *infrastructure* errors (bad request, auth failure, your service is down). A declined card
   is **not** an HTTP error.
3. **Idempotent.** Every mutating call carries an `Idempotency-Key`. The same key must always
   produce the same result — LoyaltyPlant retries on transient failures and must never
   double-charge.
4. **Synchronous when possible, asynchronous when needed.** If a payment needs 3-D Secure or a
   redirect, you return `PENDING` with a `redirectUrl`, then report the final result with the
   asynchronous callback.
5. **Reconcilable.** If an outcome is ever ambiguous (timeout, lost callback), LoyaltyPlant can
   ask your PSP for the truth via `POST /v1/inquire-status` — or, for a card binding,
   `POST /v1/inquire-enrollment-status`.

These five apply unchanged to **card enrollment**: it is capability-gated (`TOKENIZATION`),
returns its outcome in a `200` body, is idempotent, suspends on a redirect, and is
reconcilable. See [Card enrollment](03-payment-flows.md#5-card-enrollment-and-tokenization).

## 2. What you provide to LoyaltyPlant

| You provide | Notes |
| --- | --- |
| Your service **base URL** | Public HTTPS endpoint LoyaltyPlant will call (e.g. `https://payments.your-domain.example`). LoyaltyPlant appends `/v1/...`. |
| Supported **`apiVersion`** | The version segment of the contract you implement (currently `v1`). |
| Declared **capabilities** | Must match what `GET /v1/capabilities` returns. |
| **Egress IPs** (optional) | Source IPs your service calls LoyaltyPlant from, if you want them allow-listed. |
| **Required `settings` keys** | Any PSP-specific config (beyond `currency`) you need LoyaltyPlant to pass on each request — e.g. `merchantId`, processing IDs. |
| **Operational contacts** | On-call / incident contacts and your maintenance windows. |

## 3. What LoyaltyPlant provisions for you

LoyaltyPlant configures the following and shares the secrets with you over a secure channel:

| LoyaltyPlant provides | What it's for |
| --- | --- |
| **`integrationId`** | Your integration's identifier; appears in the result-callback URL `/gate/integration/{integrationId}/result`. |
| **Outbound token** | Bearer token LoyaltyPlant sends you on **every** request. You validate it (see §4). |
| **Inbound token** | Bearer token **you** send LoyaltyPlant on the result and enrollment callbacks. |
| **`settings` map** | The per-client/outlet key/values (e.g. `merchantId`) provisioned by LoyaltyPlant at onboarding and delivered on each operation (`currency` always present, plus any keys you requested). You receive it; you don't define it. |
| **Callback host** | The payments-service host you POST results to. |
| **Limits** | Rate, concurrency, and timeout limits LoyaltyPlant enforces in front of your service (see §7). |

> Internally, LoyaltyPlant stores these in its own configuration system — that's not your
> concern. From your side, they simply arrive during onboarding; treat the tokens as secrets.

## 4. Token directionality (read this twice)

There are **two** tokens and they travel in **opposite** directions. Mixing them up is the
most common integration mistake.

```mermaid
flowchart LR
    LP["LoyaltyPlant"] -->|"Outbound token<br/>you validate on every /v1/* request (else 401)"| You["YOUR service"]
    You -->|"Inbound token<br/>you send on the result callback"| LP
```

| | Outbound token | Inbound token |
| --- | --- | --- |
| **Direction** | LoyaltyPlant → you | You → LoyaltyPlant |
| **Where it appears** | `Authorization` on incoming `/v1/*` requests | `Authorization` on `POST /gate/integration/{id}/result` and `.../enrollment-result` |
| **Your job** | **Validate** it (constant-time); reject others with `401` | **Send** it on the callback |
| **If wrong** | You accept unauthenticated traffic (security hole) | LoyaltyPlant returns `401` and the payment never resumes |

Store both as secrets (vault / secret manager), never in source control or logs. Rotate via
LoyaltyPlant; support overlapping old+new during rotation so there's no downtime.

## 5. Security

- **TLS** with a valid certificate on your service; reject plaintext.
- **Validate the outbound token** on every `/v1/*` request, using a constant-time comparison;
  reject mismatches with `401` (see §4).
- **Protect the inbound token** (your callback credential) like any secret; never log it.
- **Verify PSP webhook signatures** before acting on a PSP notification — never trust an
  unverified payload (it drives the result callback).
- **Durable idempotency store**, keyed per integration (the `Idempotency-Key` contract is in
  [the spec](/payments/api/)).
- Consider **IP allow-listing** LoyaltyPlant's egress ranges (provided during onboarding).
- Never log full PANs or tokens; keep `reason` strings free of sensitive data.

## 6. Environments

LoyaltyPlant provisions you separately in a **sandbox/staging** environment and in
**production**. Each environment has its **own** `integrationId`, tokens, callback host, and
limits — never reuse staging credentials in production. Point your service at your PSP's
sandbox in staging and at the live PSP in production. Complete certification in staging first.

## 7. Operational limits

LoyaltyPlant protects both sides with a rate limiter, a concurrency bulkhead, a circuit
breaker, and request timeouts in front of your service. Typical default limits (LoyaltyPlant
sets the actual values per integration during onboarding):

| Limit | Default | What it means for you |
| --- | --- | --- |
| Request timeout | **10 s** | Respond well within this; slow responses are abandoned and treated as retriable. |
| Connect timeout | **2 s** | Your TLS/connection setup must be prompt. |
| Rate limit | **100 / s** | Sustained request rate ceiling. |
| Max concurrency | **25** | Concurrent in-flight requests LoyaltyPlant will run against you. |
| Circuit-breaker threshold | **5** | Repeated failures trip a breaker that briefly stops traffic to protect you. |

Design for these: keep synchronous endpoints fast (push slow work to the async callback),
make operations idempotent, and keep `health` honest so the breaker and routing work for you.

## 8. Glossary

| Term | Meaning |
| --- | --- |
| **Integration** | The standalone REST service you host that implements this API. |
| **integrationId** | The identifier LoyaltyPlant assigns to your integration; appears in the result-callback URL (see §3). |
| **PSP** | Payment service provider — the gateway/acquirer behind your integration (your existing system). |
| **Capability** | A feature flag you declare (`CAPTURE`, `SALE`, `DIRECT_REFUND`, `REDIRECT_FLOW`, `DIRECT_WEBHOOK`, `STATUS_POLLING`, `TOKENIZATION`, `APPLE_PAY`, `GOOGLE_PAY`). Gates which operations LP calls. |
| **Authorize** | Reserve/hold funds without capturing them (first phase of a two-phase payment). |
| **Capture** | Settle funds previously held by an authorize. |
| **Sale** | Authorize **and** capture in a single step (single-phase payment). |
| **Reverse** | Void/cancel an authorization **before** capture (releases the hold). |
| **Refund** | Return funds **after** capture. |
| **transactionReference** | Your identifier for a payment at the PSP. You return it; LP stores it and uses it for follow-up operations and to match async callbacks. |
| **outbound token** | Bearer token LP sends you on every request; you validate it (see §4). |
| **inbound token** | Bearer token you send LP on the result and enrollment callbacks (see §4). |
| **Idempotency-Key** | UUID on every mutating request; repeats must return the same result. |
| **Redirect flow** | A payment that needs the customer to complete authentication on another page (e.g. 3-D Secure) before it resolves. |
| **Result callback** | The `POST /gate/integration/{id}/result` request **you** send LP after an async payment resolves. |
| **Status inquiry** | `POST /v1/inquire-status` — LP asks you for the PSP's current status of a payment (reconciliation). |
| **Enrollment** | Binding a customer's card so it can be charged later without re-entering details. Started by `POST /v1/enroll`; gated by `TOKENIZATION`. |
| **Enrollment callback** | The `POST /gate/integration/{id}/enrollment-result` request **you** send LP to deliver a card token. The only channel that delivers one. |
| **`enrollmentId`** | LP's identifier for an enrollment, sent in `/v1/enroll` and echoed back in the callback. The one key both sides always share. |
| **paymentInstrument** | Optional field in authorize/sale naming what to charge: a `CARD_TOKEN`, or an `APPLE_PAY`/`GOOGLE_PAY` `walletPayload`. |
| **Card token** | The reference to a bound card that you issue at enrollment and LP sends back as `paymentInstrument.token`. A secret — never log it. |
| `SUCCESS` / `PENDING` / `DECLINED` / `RETRIABLE` / `UNKNOWN` | The five operation outcomes carried in the `status` field. See [Payment Flows](03-payment-flows.md). |

---

**Next:** [Payment Flows →](03-payment-flows.md)
