# Connect an independent application to Slew

Your app owns its users, license records, activation, expiry, and premium features. Slew owns its listing, checkout, payment records, and delivery retries. There is no required SDK: implement four HTTPS endpoints using any server language and any hosting provider. Never give your app access to Slew's database or import Slew's backend modules.

Download the [OpenAPI 3.1 contract](/integrations/external-licenses-v1.openapi.json). The standalone [reference app](../../examples/external-license-app/README.md) implements the protocol using FastAPI and its own PostgreSQL database with Alembic migrations. It is an example implementation, not a required dependency.

## Quickstart

1. Run or implement your app's partner endpoints at one base URL. Use HTTPS outside local development. The base URL must remain reachable by Slew's backend. For a local Compose network, use the reference app's network address rather than `localhost` inside a different container.
2. Generate a server-only credential for the intended connection. A live-capable credential must authorize both live and isolated test requests, so Slew can self-test it before live use. An optional test-only credential may authorize test requests only. Slew stores one credential per connection. Keep credentials in server configuration. Never expose them in browser JavaScript, client bundles, screenshots, URLs, or a source repository.
3. Sign in to Slew as the seller. Add an app connection with the base URL and its credential, validate the connection, then select an immutable app plan while publishing the External App product. Set the commercial price in Slew. The external plan response contains no price.
4. Make a test purchase through Slew's test payment flow. Open the purchased license from the buyer's library, sign in to your independent app, and redeem the code. Confirm the real premium feature works. Complete test acceptance before publishing live sales. A live connection uses its one live-capable credential for both the isolated self-test and later live requests; no second credential is required.

Use environment variables containing your own values in these copyable examples. Test mode accepts a live-capable credential or an optional test-only credential. Live mode accepts only a live-capable credential. The `X-Slew-Mode` header selects the data namespace; credential capability never changes the requested mode.

```sh
export APP_BASE_URL='https://your-app.example'
export APP_MODE='test'
# Set APP_API_KEY in your shell or secrets manager; do not paste it into source files.
curl --fail-with-body "$APP_BASE_URL/plans" \
  -H "Authorization: Bearer $APP_API_KEY" -H "X-Slew-Mode: $APP_MODE"
```

Example response (IDs are illustrative; retain your published revisions):

```json
{
  "protocol_version": "1",
  "integration_id": "your-app",
  "app_name": "Your app",
  "plans": [{
    "id": "premium-year", "revision": "1", "name": "Premium for one year",
    "features": ["Premium CSV exports"],
    "term": {"kind": "fixed_duration", "duration_seconds": 31536000, "starts_at": "activation"},
    "redemption_url": "https://your-app.example/redeem?mode=test",
    "redemption_instructions": "Sign in, then redeem your license code.",
    "available": true
  }]
}
```

The caller supplies a stable purchase identity. Slew computes `input_fingerprint` from `purchase_id`, `customer_reference`, `mode`, `plan_id`, and `plan_revision`; treat it as opaque, persist it, and compare exactly. `customer_reference` is an opaque principal identifier, never a customer email. The examples below demonstrate the wire format; they do not authorize issuing licenses for invented commercial purchases in Slew.

```sh
curl --fail-with-body "$APP_BASE_URL/licenses" \
  -H "Authorization: Bearer $APP_API_KEY" -H "X-Slew-Mode: test" \
  -H 'Content-Type: application/json' \
  --data '{"protocol_version":"1","purchase_id":"your-test-purchase","customer_reference":"opaque-principal","mode":"test","plan_id":"premium-year","plan_revision":"1","input_fingerprint":"example-immutable-fingerprint","desired_access":{"state":"active","revision":0}}'

curl --fail-with-body "$APP_BASE_URL/licenses/by-purchase/your-test-purchase" \
  -H "Authorization: Bearer $APP_API_KEY" -H 'X-Slew-Mode: test'

curl --fail-with-body -X PUT "$APP_BASE_URL/licenses/by-purchase/your-test-purchase/access" \
  -H "Authorization: Bearer $APP_API_KEY" -H 'X-Slew-Mode: test' \
  -H 'Content-Type: application/json' \
  --data '{"mode":"test","state":"revoked","revision":1,"input_fingerprint":"example-immutable-fingerprint"}'
```

Server-side JavaScript using the platform HTTP client:

```js
const mode = 'test';
const base = process.env.APP_BASE_URL;
const headers = {
  Authorization: `Bearer ${process.env.APP_API_KEY}`,
  'X-Slew-Mode': mode,
  'Content-Type': 'application/json',
};
const response = await fetch(`${base}/plans`, { headers });
if (!response.ok) throw new Error(`Partner request failed: ${response.status}`);
const { plans } = await response.json();
// Fetch POST/GET/PUT endpoints with the same headers. Reuse the persisted
// purchase identity for every retry; never generate a replacement purchase ID.
console.log(plans.map(({ id, revision }) => ({ id, revision })));
```

Server-side Python using the standard library:

```python
import json, os, urllib.request
base = os.environ['APP_BASE_URL'].rstrip('/')
headers = {'Authorization': 'Bearer ' + os.environ['APP_API_KEY'],
           'X-Slew-Mode': 'test', 'Content-Type': 'application/json'}
request = urllib.request.Request(base + '/plans', headers=headers)
with urllib.request.urlopen(request, timeout=15) as response:
    plans = json.load(response)['plans']
print([(plan['id'], plan['revision']) for plan in plans])
```

## Required persistence and retry behavior

- Namespace every license and access record by `mode` and `purchase_id`. A test-only credential must never read or mutate a live record, even with the same purchase ID. A live-capable credential making a test request must still read and write only test records; redeeming that test license must never unlock a live feature.
- Persist one license ID and code transactionally before responding. Retrying identical issuance at any time returns that same license. A timeout, disconnect, or HTTP 503 can follow a successful commit: recover by GET using the original purchase ID, then retry POST with that same identity if needed. Never generate a second code for a retry.
- Reject a reused purchase ID with different customer, plan, revision, or fingerprint using HTTP 409. Serialize concurrent issue/access requests in the database.
- Persist access changes even when no license exists yet. These are tombstones. A later issue request must preserve a newer suspended/revoked state and revision. A lower revision is an acknowledged no-op; equal revision with a different state is HTTP 409. Do not delete the tombstone after refund or failed delivery.
- Each license response includes `license_id`, `purchase_id`, `mode`, `plan_id`, `plan_revision`, `input_fingerprint`, `license_code`, `activation_state`, `access_state`, `access_revision`, `issued_at`, `activated_at`, and `expires_at`. Unactivated timestamps are `null`; lifetime expiry stays `null` after activation.
- First redemption binds the license to one authenticated account in your app. Same-account replay succeeds without restarting the term. A different account is denied. Apply active/suspended/revoked and expiry checks to the actual premium operation and persisted downloads, not just the redemption screen.

## Plans and compatibility

A plan's ID and revision identify an immutable promise. Lifetime plans use `{"kind":"lifetime","starts_at":"activation"}`. Fixed terms use `{"kind":"fixed_duration","duration_seconds":31536000,"starts_at":"activation"}`. Terms start at first activation in your app, never checkout or first issue. Make a new revision when changing promised features or terms. Keep historical revisions and issued licenses usable under their original promise. `available:false` stops new selection without changing existing buyers' rights. There is no automatic plan swap or retroactive lifetime conversion.

Slew snapshots the selected plan with the purchase and owns listing prices; your app must honor that purchased revision. Your app can have any internal entitlement model, provided the external protocol's durable identity, original activation, expiry, and monotonic access semantics remain intact. Retain endpoint compatibility while recoverable deliveries or historical purchases reference protocol v1. Coordinate a protocol version change for breaking semantics rather than silently changing v1.

## Ownership and launch checklist

The seller operates their application server, database backups, credential rotation, account security, license support, redemption instructions, expiry enforcement, and feature gating. Slew operates payment collection, purchase identity, listing publication, and durable partner delivery/access retries. Refunding or reversing a purchase updates access; it does not erase the immutable purchase or activation history. Temporary provider outages leave delivery pending until recovery. Never tell a buyer to buy again to repair delivery.

Before live sales, prove authenticated plan discovery, real test checkout, one code under response loss and retries, first-account redemption, same-account replay, other-account denial, actual premium output, restart persistence, expiry for fixed plans, refund/reversal denial, and isolation between test and live. Keep credentials out of evidence and clean up only explicitly owned test data. Local proof does not establish hosted live readiness.
