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

# Security Best Practices

> How to integrate with Points safely

Points treats merchant integrations as **high-trust** — API keys can create orders and issue refunds. This page collects the security practices we expect every integrator to follow.

## Transport security

* All Points endpoints are served over **HTTPS only**. Plain-HTTP requests are rejected at the edge.
* Use a client that validates the TLS certificate chain by default. Never disable certificate verification in production code.
* Minimum TLS version: **TLS 1.2**. TLS 1.3 is preferred where your HTTP client supports it.

<Warning>
  If you hit SSL errors in sandbox, fix the client's CA bundle — **do not** set `verify=False` / `rejectUnauthorized: false` / `CURLOPT_SSL_VERIFYPEER=0`.
</Warning>

## API key handling

See [API keys](/authentication/api-keys) for issuance and rotation. The short version:

* **Public key** — safe in browser/mobile. Used only for the Checkout redirect endpoint.
* **Private key** — server-side only. Loaded from a secrets manager or environment variable. Never committed to source, never returned to the client, never logged in plaintext.
* Rotate immediately if a key may have been exposed. Old keys are revoked instantly on regeneration.

## Webhook security

Points delivers webhook events with a shared secret in the header:

```http theme={null}
POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-Webhook-Secret: <secret you registered>
X-Webhook-Event: approved

{ "event": "approved", "order": { ... }, "timestamp": "..." }
```

### What to verify on every webhook

<Steps>
  <Step title="Check the X-Webhook-Secret header">
    Compare it against the secret you stored when registering the webhook. Reject any request where the header is missing or does not match.
  </Step>

  <Step title="Use constant-time comparison">
    In PHP use `hash_equals()`; in Node use `crypto.timingSafeEqual()`; in Python use `hmac.compare_digest()`. A naïve `===` leaks timing information.
  </Step>

  <Step title="Return 2xx quickly, then process asynchronously">
    Acknowledge within a few seconds. Push the payload to a queue and let a worker handle the business logic. Slow responses cause retries and duplicates.
  </Step>

  <Step title="Make your handler idempotent">
    Keyed by `order.id` (UUID) + `event`. The same event may be delivered more than once on retry.
  </Step>
</Steps>

See [Webhooks overview](/webhooks/overview#handling-webhooks) for full verification examples.

### What NOT to do

<Warning>
  * Do not expose your webhook URL on an unauthenticated public path without secret verification.
  * Do not trust the payload without checking the secret header — anyone can POST JSON.
  * Do not store the secret in source control or ship it to browser clients.
</Warning>

## Idempotency

Use `order_number` as your deduplication key. If you retry a call with the same `order_number` inside the distributed-lock window, you will receive a `429` — retry after a short back-off rather than issuing parallel calls.

On the webhook side, the same `(order.id, event)` pair may be delivered more than once. Your handler must tolerate this safely (e.g. by checking whether you've already recorded the event in your DB before acting).

## Network & infrastructure

* **Outbound from your backend** — Points API requests are standard HTTPS on port 443. No special firewall holes required.
* **Inbound (your webhook endpoint)** — must accept HTTPS POST from Points' egress. IP allow-listing is optional; the primary trust boundary is the `X-Webhook-Secret` header.
* **Timeout budget** — the Points webhook worker times out at 10 seconds and retries up to 3 times. Keep your endpoint response under \~2 seconds.

## Logging & redaction

* **Never log the Private API key or `x-api-key` header**. Configure request loggers (e.g. axios interceptors, Laravel `Http::fake()` transcripts, nginx access logs) to redact it.
* **Never log the `X-Webhook-Secret` header** for the same reason.
* PII: `phone_number` is personally identifiable — redact it from logs unless your logging store meets your compliance requirements.

## Rate limits & concurrency

* Earning order creation is serialised per merchant with a **distributed lock**. Concurrent calls for the same merchant receive `429`. Design your checkout completion pipeline to be serial, not parallel.
* Other endpoints do not currently enforce a hard rate limit, but abusive traffic will be throttled upstream. Keep production traffic well-shaped.

## Checklist

<AccordionGroup>
  <Accordion title="✅ Production readiness">
    * Private key stored in a secrets manager
    * No API key in any committed file or client bundle
    * HTTPS enforced on your webhook endpoint
    * `X-Webhook-Secret` verified on every incoming webhook with constant-time comparison
    * Webhook handler returns 2xx within 2 seconds and processes asynchronously
    * Webhook handler is idempotent on `(order.id, event)`
    * Logging redacts `x-api-key`, `X-Webhook-Secret`, and PII
    * Key rotation runbook documented internally
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Webhooks overview" icon="shield-check" href="/webhooks/overview">
    Delivery mechanics plus the recommended consumer pattern (verify, ack, deduplicate).
  </Card>

  <Card title="Go-live checklist" icon="check-double" href="/testing/go-live-checklist">
    Final checks before flipping production traffic.
  </Card>
</CardGroup>
