> ## 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.

# Integration and sample code

> Implementation requirements and end-to-end examples for the Swapnice SDK and Partner API.

This page is the implementation path for a V1 partner integration: register an app, complete OAuth, create a customer, collect consent, send events, and read a profile.

## Requirements

<AccordionGroup>
  <Accordion title="Partner application">
    A confidential backend that can store `client_id` and `client_secret`. Public/browser clients can start connection sessions but must not hold the client secret.
  </Accordion>

  <Accordion title="Exact redirect URIs">
    Every `redirect_uri` must match a registered URI exactly. Wildcards are not accepted in production.
  </Accordion>

  <Accordion title="OAuth 2.1 with PKCE">
    Authorization-code flow only. `code_challenge_method` must be `S256`. Access tokens are short-lived (about 15 minutes). Refresh tokens rotate when `offline_access` is granted.
  </Accordion>

  <Accordion title="Consent before events">
    `POST /v1/events` is rejected without a valid consent receipt for the event's `consent_purpose`.
  </Accordion>

  <Accordion title="Idempotency">
    Send an `Idempotency-Key` header on mutating calls. Replays return the original result. A reused key with a different body returns `409 idempotency_conflict`.
  </Accordion>

  <Accordion title="Scopes">
    Request only the scopes you will call. A typical profile-reading integration uses `connections:write`, `consent:write`, `events:write`, `profile:read`, `claims:read`, and `intents:read`.
  </Accordion>
</AccordionGroup>

## Recommended scope templates

| Template           | Scopes                                                                                             | Use                        |
| ------------------ | -------------------------------------------------------------------------------------------------- | -------------------------- |
| Ingest only        | `consent:write` `events:write`                                                                     | Send consented events      |
| Connect and ingest | `connections:write` `connections:read` `consent:write` `consent:read` `events:write` `events:read` | Full SDK connect flow      |
| Profile reads      | previous + `profile:read` `claims:read` `intents:read`                                             | Read resolved intelligence |
| Full partner       | previous + `customers:write` `customers:read` `webhooks:write` `webhooks:read`                     | Production integration     |

## 1. Configure the client

<CodeGroup>
  ```ts TypeScript theme={null}
  import { SwapniceClient } from "swapnice-sdk-ts";

  const swapnice = new SwapniceClient({
    environment: process.env.SWAPNICE_ENV === "production" ? "production" : "sandbox",
    clientId: process.env.SWAPNICE_CLIENT_ID!,
    clientSecret: process.env.SWAPNICE_CLIENT_SECRET!,
    redirectUri: process.env.SWAPNICE_REDIRECT_URI!,
  });
  ```

  ```python Python theme={null}
  from swapnice import Swapnice

  client = Swapnice(
      environment="sandbox",
      client_id=os.environ["SWAPNICE_CLIENT_ID"],
      client_secret=os.environ["SWAPNICE_CLIENT_SECRET"],
      redirect_uri=os.environ["SWAPNICE_REDIRECT_URI"],
  )
  ```

  ```bash cURL theme={null}
  export SWAPNICE_API_BASE_URL=https://api.sandbox.swapnice.com
  export SWAPNICE_CLIENT_ID=swa_client_...
  export SWAPNICE_CLIENT_SECRET=swa_secret_...
  export SWAPNICE_REDIRECT_URI=https://partner.example.com/swapnice/callback
  ```
</CodeGroup>

## 2. Create or upsert the partner customer

The customer is **your** user. You do not need their global Swapnice user id.

<CodeGroup>
  ```ts TypeScript theme={null}
  const { customer } = await swapnice.customers.upsert({
    external_id: "partner_user_1842",
    profile: { username: "collector1842" },
    metadata: { loyalty_tier: "gold", source: "ios" },
  });
  ```

  ```bash cURL theme={null}
  curl -X POST "$SWAPNICE_API_BASE_URL/v1/customers" \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: cust_partner_user_1842" \
    -d '{
      "external_id": "partner_user_1842",
      "profile": { "username": "collector1842" },
      "metadata": { "loyalty_tier": "gold", "source": "ios" }
    }'
  ```
</CodeGroup>

Example response:

```json theme={null}
{
  "status": "created",
  "customer": {
    "id": "cust_8f2a1c",
    "app_id": "app_91b0",
    "external_id": "partner_user_1842",
    "status": "active",
    "profile": { "username": "collector1842" },
    "metadata": { "loyalty_tier": "gold", "source": "ios" },
    "created_at": "2026-08-26T16:01:00.000Z",
    "updated_at": "2026-08-26T16:01:00.000Z"
  }
}
```

<Note>
  Customer `profile` accepts public display fields only (`username`, `bio_markdown`, `avatar_url`, `banner_url`, coarse public location). Do not send payment, government-id, or credential data.
</Note>

## 3. Start a connection session

This is the Plaid-Link-style step: your app opens a Swapnice-hosted connection URL so the user can authorize the link.

<CodeGroup>
  ```ts TypeScript theme={null}
  const session = await swapnice.connectionSessions.create({
    customer_id: customer.id,
    requested_purposes: ["personalization", "analytics"],
    redirect_uri: process.env.SWAPNICE_REDIRECT_URI!,
  });

  // Open session.connection_url in-app or in the system browser.
  ```

  ```bash cURL theme={null}
  curl -X POST "$SWAPNICE_API_BASE_URL/v1/connection-sessions" \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: cs_partner_user_1842_1" \
    -d '{
      "customer_id": "cust_8f2a1c",
      "requested_purposes": ["personalization", "analytics"],
      "redirect_uri": "https://partner.example.com/swapnice/callback"
    }'
  ```
</CodeGroup>

```json theme={null}
{
  "session_id": "cs_0b77e2",
  "client_token": "cst_...",
  "connection_url": "https://api.sandbox.swapnice.com/connect/cs_0b77e2",
  "status": "created",
  "expires_at": "2026-08-26T16:06:00.000Z"
}
```

Poll `GET /v1/connection-sessions/:session_id` until `status` is `authorized` or `completed`.

## 4. Record the consent receipt

After the user accepts, persist the receipt. Events for a purpose are rejected until this exists.

<CodeGroup>
  ```ts TypeScript theme={null}
  const consent = await swapnice.consentReceipts.create({
    customer_id: customer.id,
    connection_session_id: session.session_id,
    purposes: ["personalization", "analytics"],
    terms_version: "2026-08-01",
    privacy_policy_version: "2026-08-01",
  });
  ```

  ```bash cURL theme={null}
  curl -X POST "$SWAPNICE_API_BASE_URL/v1/consent-receipts" \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: cr_partner_user_1842_1" \
    -d '{
      "customer_id": "cust_8f2a1c",
      "connection_session_id": "cs_0b77e2",
      "purposes": ["personalization", "analytics"],
      "terms_version": "2026-08-01",
      "privacy_policy_version": "2026-08-01"
    }'
  ```
</CodeGroup>

```json theme={null}
{
  "receipt_id": "cr_44aa01",
  "customer_id": "cust_8f2a1c",
  "purposes": ["personalization", "analytics"],
  "status": "granted",
  "granted_at": "2026-08-26T16:02:12.000Z"
}
```

## 5. Ingest a consented event

The SDK can emit this from an observer. A backend can POST the same payload. Either way, include `consent_purpose` and an `Idempotency-Key`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const accepted = await swapnice.events.create(
    {
      customer_id: customer.id,
      client_event_id: "evt_checkout_8891",
      consent_receipt_id: consent.receipt_id,
      consent_purpose: "personalization",
      event_type: "purchase.completed",
      ontology: "intent",
      ontology_category: "outcome",
      object: "sku_blue_eyes_tin",
      occurred_at: new Date().toISOString(),
      payload: { sku: "sku_blue_eyes_tin", currency: "USD", amount: 24.99 },
      context: { surface: "checkout", storefront: "ios" },
      confidence: 1,
      provenance: {
        source_partner: "partner_app",
        source_system: "ios",
        collected_at: new Date().toISOString(),
      },
    },
    { headers: { "Idempotency-Key": "evt_checkout_8891" } },
  );
  ```

  ```bash cURL theme={null}
  curl -X POST "$SWAPNICE_API_BASE_URL/v1/events" \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: evt_checkout_8891" \
    -d '{
      "customer_id": "cust_8f2a1c",
      "client_event_id": "evt_checkout_8891",
      "consent_receipt_id": "cr_44aa01",
      "consent_purpose": "personalization",
      "event_type": "purchase.completed",
      "ontology": "intent",
      "ontology_category": "outcome",
      "object": "sku_blue_eyes_tin",
      "occurred_at": "2026-08-26T16:04:00.000Z",
      "payload": { "sku": "sku_blue_eyes_tin", "currency": "USD", "amount": 24.99 },
      "context": { "surface": "checkout", "storefront": "ios" },
      "confidence": 1,
      "provenance": {
        "source_partner": "partner_app",
        "source_system": "ios",
        "collected_at": "2026-08-26T16:04:00.000Z"
      }
    }'
  ```
</CodeGroup>

`202` response. The event is accepted and queued; heavier projection is asynchronous.

```json theme={null}
{
  "event_id": "evt_9c21",
  "status": "accepted",
  "sequence": 7,
  "accepted_at": "2026-08-26T16:04:00.120Z",
  "processing_status": "queued"
}
```

Batch the same shape to `POST /v1/events/batch` (1–100 events).

## 6. Read the resolved profile and intents

Reads are synchronous and scoped to the caller plus optional `consent_purpose`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const profile = await swapnice.profiles.get("cust_8f2a1c", {
    consent_purpose: "personalization",
  });

  const intents = await swapnice.profiles.intents("cust_8f2a1c", {
    consent_purpose: "personalization",
    category: "outcome",
  });
  ```

  ```bash cURL theme={null}
  curl "$SWAPNICE_API_BASE_URL/v1/profiles/cust_8f2a1c?consent_purpose=personalization" \
    -H "Authorization: Bearer $ACCESS_TOKEN"

  curl "$SWAPNICE_API_BASE_URL/v1/profiles/cust_8f2a1c/intents?consent_purpose=personalization&category=outcome" \
    -H "Authorization: Bearer $ACCESS_TOKEN"
  ```
</CodeGroup>

Sample profile and intent bodies are on [outputs](/partners/outputs).

## 7. Register a webhook endpoint

```ts theme={null}
const { endpoint, signing_secret } = await swapnice.webhookEndpoints.create({
  url: "https://partner.example.com/swapnice/webhooks",
  enabled_events: ["event.processed", "event.failed", "consent.revoked", "profile.updated"],
});
```

Delivery of those events is near-term. Registration and secret issuance are live so you can build the receiver during the pilot.

## OAuth token exchange

If you are not using the SDK auth helper, the raw PKCE exchange is:

```bash theme={null}
# 1. Authorization code
curl -X POST "$SWAPNICE_API_BASE_URL/oauth/authorize" \
  -H "Content-Type: application/json" \
  -d '{
    "response_type": "code",
    "client_id": "'"$SWAPNICE_CLIENT_ID"'",
    "redirect_uri": "'"$SWAPNICE_REDIRECT_URI"'",
    "code_challenge": "'"$CODE_CHALLENGE"'",
    "code_challenge_method": "S256",
    "customer_id": "cust_8f2a1c",
    "scope": "connections:write consent:write events:write profile:read claims:read intents:read"
  }'

# 2. Token
curl -X POST "$SWAPNICE_API_BASE_URL/oauth/token" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "client_id": "'"$SWAPNICE_CLIENT_ID"'",
    "client_secret": "'"$SWAPNICE_CLIENT_SECRET"'",
    "code": "'"$AUTH_CODE"'",
    "code_verifier": "'"$CODE_VERIFIER"'",
    "redirect_uri": "'"$SWAPNICE_REDIRECT_URI"'"
  }'
```

Discovery lives at `GET /.well-known/oauth-authorization-server`.

## Error shape

All Partner API errors use a stable machine-readable code and a request id. They never include secrets or private wallet data.

```json theme={null}
{
  "error": {
    "code": "consent_required",
    "message": "A valid consent receipt is required for this event purpose.",
    "request_id": "req_3e19c0"
  }
}
```

Common codes: `idempotency_key_required`, `idempotency_conflict`, `consent_required`, `invalid_scope`, `invalid_redirect_uri`.

## Next

* [Authentication](/partners/authentication) — PKCE, scopes, and token errors
* [Consent and account linking](/partners/consent)
* [Events and intent ontology](/partners/events-and-intents)
* [Request walkthrough](/partners/request-walkthrough)
* [What data is collected](/partners/data-collection)
* [API reference](/partners/api-overview)
