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

# Going live

> The checklist to run before you flip the same flow at api.ledgersyncappv2.com with a live key.

Your sandbox integration and your live integration are the *same code* against
two base URLs. Going live is mostly about hardening the webhook handler and
swapping the key. Walk this checklist before your first real user.

<Info>
  **Live base URL:** `https://api.ledgersyncappv2.com/v3`  · 
  **Key prefix:** `sk_live_...`. Sandbox keys (`sk_test_...`) do **not**
  authenticate against the live base URL, and vice-versa.
</Info>

## Before your first live user

<Steps>
  <Step title="Mint a live API key">
    Same flow as sandbox, but choose **live** as the environment. Copy the
    `sk_live_...` value once, it's shown only at creation, and store it in your
    server's secret store. See [Authentication](/authentication).
  </Step>

  <Step title="Store the signing secret somewhere durable">
    The `signing_secret` from your webhook subscription is shown **exactly
    once**. Persist it to a real secrets manager (GCP Secret Manager, AWS
    Secrets Manager, 1Password Connect, whatever you already run) before you
    close the tab. You need it to verify every delivery.
  </Step>

  <Step title="Dedupe on event_id">
    We retry failed deliveries with exponential backoff for **24 hours**, so
    the same event can land more than once. Keep a short-lived set of seen
    `event_id`s and short-circuit duplicates. Your handler must be idempotent.
  </Step>

  <Step title="Return 2xx fast">
    Acknowledge each delivery within **30 seconds**. If your work is slow (DB
    writes, notifications, downstream calls), enqueue it and return `200`
    immediately, anything else looks like a failure and triggers a retry.
  </Step>

  <Step title="Verify signatures constant-time">
    Compare the HMAC with `hmac.compare_digest` (Python) /
    `crypto.timingSafeEqual` (Node) / your language's equivalent, never a plain
    `==`, which leaks timing. Reject any delivery whose
    `X-LS-Webhook-Timestamp` is more than **5 minutes** old. Full recipe in
    [Webhooks](/guides/webhooks).
  </Step>

  <Step title="Exercise your handler with webhook.test">
    Fire `POST /v3/webhooks/subscriptions/{id}/test` any time. It's signed with
    the subscription's real `signing_secret` and carries the same headers as
    production, so your verification path runs end-to-end. It bypasses the
    `event_types` filter, so you don't need to subscribe to `webhook.test` for
    it to land.
  </Step>

  <Step title="Plan for connection.failed and connection.disconnected">
    Banks reject credentials, MFA tokens expire, users revoke access. Subscribe
    to both events and surface a clear **re-link** call to action in your UI
    when either fires. See [Connection lifecycle](/guides/connection-lifecycle).
  </Step>

  <Step title="Log X-LS-Trace-Id everywhere">
    Every response, including errors, carries an `X-LS-Trace-Id`. Log it. When
    you open a support ticket, quote it and we jump straight to your request.
  </Step>
</Steps>

<Warning>
  The single most common go-live bug is a webhook handler that isn't
  idempotent or is too slow to ack. Get dedupe-on-`event_id` and
  return-2xx-fast right in sandbox with [`webhook.test`](/guides/testing) before
  you ship.
</Warning>

## What changes from sandbox

|                 | Sandbox                                        | Live                         |
| --------------- | ---------------------------------------------- | ---------------------------- |
| Base URL        | `api-sandbox.ledgersyncappv2.com/v3`           | `api.ledgersyncappv2.com/v3` |
| Key prefix      | `sk_test_...`                                  | `sk_live_...`                |
| Banks           | Aggregator sandbox banks + FDE Ledgersync Bank | Real financial institutions  |
| Everything else | —                                              | Identical                    |

<Check>
  Same endpoints, same webhook shapes, same routing. If it worked against the
  [FinBank smoke test](/guides/testing), it works live.
</Check>

## Rate limits

Every API key is rate-limited per environment. The **live** tier trades a lower
sustained rate for a much larger daily budget and a short burst allowance; the
**sandbox** tier lets you hammer it faster while you build but caps the daily
total.

| Limit             | Sandbox | Live        |
| ----------------- | ------- | ----------- |
| Requests / minute | 120     | 60          |
| Requests / day    | 10,000  | 50,000      |
| Burst             | none    | 2× over 60s |

Every response, success or error, carries these headers:

| Header                  | Meaning                                              |
| ----------------------- | ---------------------------------------------------- |
| `X-RateLimit-Limit`     | Requests allowed in the per-minute window            |
| `X-RateLimit-Remaining` | Lowest remaining count across all active windows     |
| `X-RateLimit-Reset`     | Unix seconds when the per-minute window resets       |
| `X-RateLimit-Policy`    | Every enforced window, e.g. `60;w=60, 50000;w=86400` |

Read `X-RateLimit-Remaining` and back off *before* you hit zero rather than
waiting for the `429`. Because live gets a 2× burst, a single spike can pass
while the minute window is technically full, then the next request trips.

### When you get throttled

Over the limit, the API returns `429` with the standard error envelope and a
`Retry-After` header (seconds):

```json theme={null}
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded"
  }
}
```

```
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Policy: 60;w=60, 50000;w=86400
```

Honor `Retry-After` exactly: on a per-minute trip it's the seconds to the next
minute boundary; on a daily-budget trip it's the seconds until the day rolls
over, which can be hours.

### Service-layer caps

Some limits are enforced deeper in the stack and are **not** reflected in the
`X-RateLimit-*` headers. You'll still see a `429` (`rate_limit_exceeded`) when
you cross them:

| Cap                              | Sandbox | Live   |
| -------------------------------- | ------- | ------ |
| Concurrent open connection flows | 10      | 50     |
| Webhook events / day             | 5,000   | 25,000 |
| Statement extractions / day      | 50      | 500    |
| Concurrent statement extractions | 2       | 3      |

<Info>
  Statement extractions are capped separately because each one burns a real
  OCR-provider call, even on sandbox keys. Queue extraction jobs and cap your
  own concurrency rather than firing them all at once.
</Info>

<Warning>
  Don't hardcode these numbers into retry logic. Drive backoff off
  `X-RateLimit-Remaining` and `Retry-After` so your integration keeps working
  if a tier changes.
</Warning>
