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

# Sandbox and testing

> Environments, credentials, and the expected implementation and test process for a Swapnice pilot.

Partners integrate against sandbox first. Production credentials are issued after the sandbox checklist passes.

## Environments

|             | Sandbox                                            | Production                                        |
| ----------- | -------------------------------------------------- | ------------------------------------------------- |
| API base    | `https://api.sandbox.swapnice.com`                 | `https://api.swapnice.com`                        |
| Data        | Isolated fixtures and partner-generated test users | Live consented traffic                            |
| Secrets     | Separate `client_id` / `client_secret`             | Separate credentials; never reuse sandbox secrets |
| Rate limits | Generous for integration                           | Contractual                                       |
| Consent     | Real protocol, test users                          | Real users, production policy versions            |

Discovery: `GET {base}/.well-known/oauth-authorization-server`

Local Worker development (`http://localhost:8787`) is available for Swapnice-internal or white-glove sessions. Partner teams should treat sandbox as the source of truth unless we schedule a shared local walkthrough.

## What you receive at kickoff

Swapnice issues:

* Sandbox `app_id`, `client_id`, and a one-time `client_secret`
* Allowed redirect URIs (you supply these first)
* Allowed scopes and consent purposes for the pilot
* A support alias for integration questions (`api@swapnice.com`)

Store the secret in your secret manager. It is shown once. Rotation is done by Swapnice until self-serve dashboard rotation ships.

Suggested environment variables:

```text theme={null}
SWAPNICE_API_BASE_URL=https://api.sandbox.swapnice.com
SWAPNICE_CLIENT_ID=swa_client_...
SWAPNICE_CLIENT_SECRET=swa_secret_...
SWAPNICE_REDIRECT_URI=https://partner.example.com/swapnice/callback
SWAPNICE_ENV=sandbox
```

## Expected implementation process

<Steps>
  <Step title="Read the contract">
    Review [SDK versus API](/partners/sdk-vs-api), [integration](/partners/integration), [data collection](/partners/data-collection), and [outputs](/partners/outputs) before the technical session.
  </Step>

  <Step title="Register redirect URIs and scopes">
    Send the exact sandbox and production redirect URIs, plus the scope template you intend to use. Swapnice configures the sandbox app.
  </Step>

  <Step title="Complete OAuth in sandbox">
    PKCE authorize → token → introspect. Confirm token expiry and refresh before writing events.
  </Step>

  <Step title="Walk one test user through connect">
    Create a customer, open `connection_url`, grant purposes, persist the receipt.
  </Step>

  <Step title="Send two event families">
    One explicit claim (for example `profile.selected`) and one behavioral event (for example `purchase.completed`). Confirm `202`, then `GET /v1/events/:id` until `processed`.
  </Step>

  <Step title="Read profile, claims, and intents">
    Call the three read routes with `consent_purpose`. Confirm facts, alternatives, and a `403` after you revoke that purpose.
  </Step>

  <Step title="Register the webhook receiver">
    Create the endpoint and verify signature handling. Live delivery is near-term; the receiver should still be ready.
  </Step>

  <Step title="Technical session">
    Bring the payloads, failing cases, and scope questions. This is the meeting the docs are meant to make productive.
  </Step>

  <Step title="Production review">
    Production app, production policy versions, go-live event list, and a revoke/suppression test.
  </Step>
</Steps>

## Sandbox checklist

Use this as the acceptance list before asking for production.

* [ ] Sandbox token introspects with the expected scopes
* [ ] Redirect URI mismatches are rejected
* [ ] Customer upsert is idempotent on `external_id`
* [ ] Connection session expires if left unused
* [ ] Events without a receipt return `consent_required`
* [ ] Replaying the same `Idempotency-Key` returns the original event
* [ ] Changing the body on that key returns `409`
* [ ] Profile read returns resolved facts after the archive processes
* [ ] Revoking a purpose blocks later events and purpose-scoped reads
* [ ] No secrets, emails, or payment tokens appear in `payload` or `metadata`
* [ ] Webhook receiver verifies the signing secret (even if delivery is still test-only)

## Suggested test fixtures

| Fixture                     | Value                                          |
| --------------------------- | ---------------------------------------------- |
| Test customer `external_id` | `sandbox_user_001`                             |
| Purposes                    | `personalization`, `analytics`                 |
| Event A                     | `profile.selected` / attribute favorite series |
| Event B                     | `purchase.completed` / a catalog SKU           |
| Negative test               | Event for a purpose that is not granted        |
| Revoke test                 | Revoke `personalization`, then retry Event B   |

## Postman and cURL

Follow the [request walkthrough](/partners/request-walkthrough) for the full ordered sequence (authorize → customer → connect → consent → event → profile → revoke).

If you use Postman:

1. Do not use the built-in OAuth 2.0 helper. Authorize is a JSON `POST` with `customer_id` and PKCE.
2. Set collection auth to Bearer `{{access_token}}` after the token call.
3. Tokens expire in about 15 minutes. Re-run authorize + token when you see `401`.
4. `POST /v1/customers` takes `external_id`, not `customer_id`.

## Common errors

| Error                      | Fix                                                         |
| -------------------------- | ----------------------------------------------------------- |
| `invalid_client_id`        | Confirm the sandbox client Swapnice issued                  |
| `invalid_grant`            | Code expired or reused — run authorize again                |
| `invalid_scope`            | Scope string must be space-separated and allowed on the app |
| `invalid_redirect_uri`     | Must exactly match a registered URI                         |
| PKCE failure               | `code_verifier` must pair with the `S256` challenge         |
| `missing_bearer_token`     | Add `Authorization: Bearer`                                 |
| `401` on `/v1/*`           | Token expired or missing scope                              |
| `consent_required`         | Grant that purpose, or stop sending it                      |
| `idempotency_key_required` | Add `Idempotency-Key` on mutating calls                     |
| `500` on `/v1/customers`   | Body field is `external_id`, not `customer_id`              |

## What "done" looks like for a pilot

The sandbox integration is complete when:

1. A test user can connect, grant, generate events, and appear in a profile read.
2. Revocation suppresses that user.
3. Your backend can call profile/intent reads without the client SDK.
4. You have a written list of production event types and purposes.

That is also the bar for scheduling the implementation working session: your engineers will have exercised the API and can ask specific questions.

## Getting help

Email [api@swapnice.com](mailto:api@swapnice.com) with:

* Sandbox `app_id` and `request_id` from the failing response
* Route, timestamp, and a redacted payload
* Whether the call came from the SDK or raw HTTP

Do not send client secrets or access tokens in email.
