# Cozzix Business Line API

**Version:** 1.0  
**Production base URL:** `https://api.cozzix.com`  
**Audience:** ANX products, SaaS applications, billing services, and server-side integrations

## 1. Purpose

The Business Line API sends financial activity from an ANX product into the ANX LLC workspace in Cozzix.

Each product receives its own **Business Line** and API key. A Business Line represents the product or operating unit that earned the revenue—for example Cozzix, Connvi, Vocallix, eSIMUp, or ANX Cloud.

The API can:

- Create a paid Cozzix invoice after a product successfully collects payment.
- Create or match the CRM client and link that client to the calling Business Line.
- Record the payment and safe payment-method details.
- Synchronize subsequent payment states, including failures, cancellations, disputes, and refunds.
- Create linked credit notes and refund records.
- Return connection health and recent delivery information.

One CRM client can be linked to multiple Business Lines. A product-specific customer ID is unique only within its Business Line.

## 2. Integration model

Use these endpoints:

| Purpose | Method | Endpoint |
|---|---:|---|
| Check credentials and connection health | `GET` | `/api/external/v1/status` |
| Record a successful payment and create its paid invoice | `POST` | `/api/external/v1/invoices` |
| Synchronize payment lifecycle changes | `POST` | `/api/external/v1/transactions` |

Recommended sequence for a successful payment:

1. The product completes payment with Stripe.
2. The product sends the paid invoice to `/invoices`.
3. The product sends later lifecycle events for the same charge to `/transactions`.
4. If a refund occurs, send the refund event only after the paid invoice has been accepted by Cozzix.

The product remains the source of truth for the payment processor action. Cozzix becomes the centralized source of truth for invoices, CRM reporting, revenue attribution, credits, and audit history.

## 3. Authentication

Every Business Line receives a separate live API key beginning with:

```text
czx_live_
```

Send it as a Bearer token:

```http
Authorization: Bearer czx_live_REPLACE_WITH_SECRET
```

Security requirements:

- Store the key only in server-side environment variables or a secrets manager.
- Never place it in client-side JavaScript, mobile applications, public repositories, logs, analytics, or error messages.
- Do not share a key between products. The key determines the ANX workspace and Business Line automatically.
- Rotate the key immediately if exposure is suspected. Rotation invalidates the previous key.
- Use HTTPS only.

Example environment variable:

```bash
COZZIX_API_KEY=czx_live_REPLACE_WITH_SECRET
```

## 4. Common headers

```http
Authorization: Bearer czx_live_REPLACE_WITH_SECRET
Content-Type: application/json
Idempotency-Key: stable-unique-event-id
```

`Idempotency-Key` is required for both POST endpoints and may contain up to 190 characters.

Recommended values:

- Stripe webhook: use the Stripe Event ID, such as `evt_123`.
- Internal billing job: use an immutable event ID, such as `connvi:payment:pay_123:completed`.
- Never generate a new idempotency key when retrying the same event.

Retries must resend the exact same request body. Reusing an idempotency key with a different body returns `409 Conflict` on both POST endpoints.

## 5. Data conventions

### Money

All amounts are decimal **major currency units**, not cents.

```json
{
  "currency": "USD",
  "amount": 79.99
}
```

Do not send `7999` for USD 79.99. Currency codes must be three-letter ISO 4217 codes and must be enabled in the ANX LLC Cozzix workspace.

### Dates

Send ISO 8601 timestamps with a timezone, preferably UTC:

```text
2026-10-20T14:25:03.000Z
```

### Countries and timezones

- `country`: two-letter ISO 3166-1 alpha-2 code, such as `US`, `IL`, or `ES`.
- `timezone`: IANA timezone, such as `America/Los_Angeles`, `America/New_York`, or `Asia/Jerusalem`.

### Card data

Allowed safe fields:

- Card brand
- Last four digits
- Expiration month and year
- Funding type
- Wallet type
- Stripe receipt URL

Never send or store:

- Full card number
- CVC/CVV
- Track data
- PIN
- Raw payment-method secrets

## 6. Customer matching

Cozzix resolves a customer in this order:

1. `customer.externalId` within the authenticated Business Line.
2. Customer email within the ANX LLC workspace.
3. Create a new CRM client when neither match exists.

After a match, Cozzix stores the Business Line ↔ external customer mapping. This allows the same CRM client to have different IDs in multiple products.

Always send a stable `customer.externalId`. Email is a fallback and may change.

## 7. Check connection status

### Request

```http
GET /api/external/v1/status
Authorization: Bearer czx_live_REPLACE_WITH_SECRET
```

No request body or idempotency key is required.

### cURL

```bash
curl --request GET \
  --url https://api.cozzix.com/api/external/v1/status \
  --header "Authorization: Bearer $COZZIX_API_KEY"
```

### Success response — `200 OK`

```json
{
  "ok": true,
  "serverTime": "2026-10-20T14:25:03.000Z",
  "businessLine": {
    "id": "7e77186f-2ca4-4e08-9a1d-2d23fd2ad100",
    "name": "Connvi",
    "code": "connvi",
    "kind": "saas",
    "stripeAccountId": "acct_123",
    "stripeAccountType": "organization",
    "lastEventAt": "2026-10-20T14:20:00.000Z"
  },
  "capabilities": {
    "paidInvoices": true,
    "transactionLifecycle": true,
    "refunds": true,
    "disputes": true,
    "idempotency": true,
    "safeCardDetails": true
  },
  "currencies": [
    { "code": "USD", "default": true }
  ],
  "totals": {
    "customers": 120,
    "invoices": 430,
    "paidInvoices": 420,
    "transactions": 447,
    "failuresOrDisputes": 3
  },
  "recentTransactions": [],
  "recentDeliveries": []
}
```

Call this endpoint during deployment and health checks. Do not use it for high-frequency polling; once every 5–15 minutes is sufficient.

## 8. Create a paid invoice

Use this endpoint only after payment succeeded. It creates:

- A CRM client or customer match
- A paid Cozzix invoice
- Invoice line items
- A payment record
- A payment transaction
- Business Line attribution and audit logs

Invoice numbers are generated by Cozzix. Callers cannot choose or alter invoice numbering.

### Request

```http
POST /api/external/v1/invoices
Authorization: Bearer czx_live_REPLACE_WITH_SECRET
Idempotency-Key: evt_1ExampleSucceeded
Content-Type: application/json
```

```json
{
  "externalInvoiceId": "in_123",
  "invoicedAt": "2026-10-20T14:25:00.000Z",
  "paidAt": "2026-10-20T14:25:03.000Z",
  "currency": "USD",
  "customer": {
    "externalId": "cus_123",
    "name": "Example Customer",
    "email": "billing@example.com",
    "phone": "+12125550123",
    "country": "US",
    "timezone": "America/Los_Angeles"
  },
  "items": [
    {
      "externalProductId": "prod_monthly",
      "description": "Monthly SaaS subscription",
      "quantity": 1,
      "unitAmount": 79.99,
      "taxPercent": 0
    },
    {
      "externalProductId": "prod_addon",
      "description": "Additional user",
      "quantity": 2,
      "unitAmount": 10,
      "taxPercent": 0
    }
  ],
  "discountTotal": 5,
  "notes": "October subscription",
  "payment": {
    "provider": "stripe",
    "method": "card",
    "accountId": "acct_123",
    "paymentId": "pi_123",
    "chargeId": "ch_123",
    "receiptUrl": "https://pay.stripe.com/receipts/example",
    "card": {
      "brand": "visa",
      "last4": "4242",
      "expMonth": 12,
      "expYear": 2030,
      "funding": "credit",
      "wallet": "apple_pay"
    }
  }
}
```

### Field reference

| Field | Type | Required | Rules |
|---|---|:---:|---|
| `externalInvoiceId` | string | No | Provider invoice ID; maximum 190 characters |
| `invoicedAt` | ISO date | No | Defaults logically to `paidAt` |
| `paidAt` | ISO date | Yes | Successful payment time |
| `currency` | string | Yes | Three-letter enabled currency |
| `customer` | object | Yes | Customer data |
| `customer.externalId` | string | Recommended | Stable ID inside the calling product |
| `customer.name` | string | Yes | 2–180 characters |
| `customer.email` | email | No | Maximum 255 characters |
| `customer.phone` | string | No | Maximum 40 characters |
| `customer.country` | string | No | Two-letter country code |
| `customer.timezone` | string | No | IANA timezone; maximum 80 characters |
| `items` | array | Yes | 1–100 line items |
| `items[].externalProductId` | string | No | Product ID; maximum 190 characters |
| `items[].description` | string | Yes | 1–500 characters |
| `items[].quantity` | number | No | Positive; default 1 |
| `items[].unitAmount` | number | Yes | Zero or positive major-unit amount |
| `items[].taxPercent` | number | No | 0–100; default 0 |
| `discountTotal` | number | No | Zero or positive; default 0 |
| `notes` | string | No | Maximum 5,000 characters |
| `payment.provider` | string | No | Default `stripe` |
| `payment.method` | string | No | Default `card`; maximum 60 characters |
| `payment.accountId` | string | Recommended | Must match the Business Line when configured |
| `payment.paymentId` | string | Yes | Stripe PaymentIntent or provider payment ID |
| `payment.chargeId` | string | Recommended | Stripe Charge or equivalent transaction ID |
| `payment.receiptUrl` | URL | No | Maximum 1,000 characters |
| `payment.card` | object | No | Safe display fields only |

Calculation:

```text
subtotal = sum(quantity × unitAmount)
tax = sum(quantity × unitAmount × taxPercent / 100)
total = subtotal + tax - discountTotal
```

The final total must be greater than zero.

### Success response — `201 Created`

```json
{
  "invoice": {
    "id": "c7dcb56a-05c8-487b-9369-470fdb626816",
    "number": "EXA-0001",
    "status": "paid",
    "total": "94.99",
    "currency": "USD"
  },
  "duplicate": false
}
```

### Repeated event or payment — `200 OK`

```json
{
  "invoice": {
    "id": "c7dcb56a-05c8-487b-9369-470fdb626816",
    "number": "EXA-0001",
    "status": "paid"
  },
  "duplicate": true
}
```

Cozzix also detects an existing invoice by the Business Line's external invoice ID, Stripe account, PaymentIntent, and Charge. This prevents two different Stripe success notifications from creating invoices for the same payment.

## 9. Synchronize a payment transaction

Use this endpoint for lifecycle events, including payments that failed before an invoice was created.

Supported statuses:

- `pending`
- `succeeded`
- `failed`
- `refunded`
- `partially_refunded`
- `disputed`
- `canceled`

### Request

```http
POST /api/external/v1/transactions
Authorization: Bearer czx_live_REPLACE_WITH_SECRET
Idempotency-Key: evt_1ExampleRefunded
Content-Type: application/json
```

```json
{
  "provider": "stripe",
  "accountId": "acct_123",
  "paymentId": "pi_123",
  "chargeId": "ch_123",
  "externalInvoiceId": "in_123",
  "status": "partially_refunded",
  "amount": 94.99,
  "amountRefunded": 20,
  "currency": "USD",
  "description": "Monthly SaaS subscription",
  "occurredAt": "2026-10-20T15:30:00.000Z",
  "paidAt": "2026-10-20T14:25:03.000Z",
  "customer": {
    "externalId": "cus_123",
    "name": "Example Customer",
    "email": "billing@example.com"
  },
  "paymentMethod": {
    "type": "card",
    "cardBrand": "visa",
    "cardLast4": "4242",
    "cardExpMonth": 12,
    "cardExpYear": 2030,
    "cardFunding": "credit",
    "wallet": "apple_pay",
    "receiptUrl": "https://pay.stripe.com/receipts/example"
  },
  "refunds": [
    {
      "id": "re_123",
      "amount": 20,
      "status": "succeeded",
      "reason": "Customer request",
      "createdAt": "2026-10-20T15:30:00.000Z"
    }
  ],
  "metadata": {
    "subscriptionId": "sub_123",
    "environment": "production"
  }
}
```

### Field reference

| Field | Type | Required | Rules |
|---|---|:---:|---|
| `provider` | string | No | Default `stripe` |
| `accountId` | string | Recommended | Must match the configured Business Line account |
| `paymentId` | string | No | PaymentIntent or provider payment ID |
| `chargeId` | string | Yes | Stable charge/transaction ID; maximum 190 characters |
| `externalInvoiceId` | string | No | Used to link the original invoice |
| `status` | enum | Yes | One of the supported statuses above |
| `amount` | number | Yes | Positive original payment amount |
| `amountRefunded` | number | No | 0–`amount`; default 0 |
| `currency` | string | Yes | Three-letter currency code |
| `description` | string | No | Maximum 500 characters |
| `occurredAt` | ISO date | Yes | Time of this event or original transaction |
| `paidAt` | ISO date | No | Original successful payment time |
| `failure` | object | No | `code`, `message`, and `declineCode` |
| `paymentMethod` | object | No | Safe payment display fields |
| `customer` | object | No | Same matching rules as the invoice endpoint |
| `refunds` | array | No | Maximum 100 Stripe refund records |
| `metadata` | object | No | Non-sensitive integration metadata |

When `refunds` is non-empty, the paid invoice and its payment must already exist in Cozzix. Otherwise the API returns retryable `409 Conflict`. Send the invoice first and retry the exact refund event with the same idempotency key.

### Success response — `201 Created` or `200 OK`

```json
{
  "transaction": {
    "id": "01f7473e-bc2f-4912-93f1-c00fde2b456e",
    "status": "partially_refunded",
    "invoiceId": "c7dcb56a-05c8-487b-9369-470fdb626816",
    "clientId": "b4e4387f-edf9-4460-a4e4-57953b45d4c7"
  },
  "duplicate": true,
  "refundsRecorded": 1
}
```

`duplicate: true` means the charge already existed and was updated. It does not indicate an error.

An exact replay of the same provider event includes:

```json
{
  "transaction": {},
  "duplicate": true,
  "refundsRecorded": 1,
  "duplicateEvent": true
}
```

## 10. Stripe event mapping

| Stripe event | Cozzix action |
|---|---|
| `invoice.paid` | Send paid invoice to `/invoices` |
| `checkout.session.completed` | Send to `/invoices` only after confirming payment is paid |
| `charge.succeeded` | Send `succeeded` to `/transactions`; skip if `/invoices` already recorded the same charge unless an explicit lifecycle log is desired |
| `payment_intent.payment_failed` | Send `failed` with failure details |
| `payment_intent.canceled` | Send `canceled` |
| `charge.refunded` | Send `refunded` or `partially_refunded` with the complete refund list |
| `charge.dispute.created` | Send `disputed` |
| `charge.dispute.closed` | Send the charge's current final state with a new Stripe Event ID |

Do not create the Cozzix invoice from both `invoice.paid` and `checkout.session.completed`. Choose one canonical success event in each product.

## 11. Error responses

Errors use JSON:

```json
{
  "error": "Human-readable error message"
}
```

Validation errors may also include fields:

```json
{
  "error": "currency: Too small: expected string to have >=3 characters",
  "fields": {
    "currency": ["Too small: expected string to have >=3 characters"]
  }
}
```

| HTTP status | Meaning | Retry? |
|---:|---|:---:|
| `200` | Accepted update or duplicate replay | No |
| `201` | New invoice or transaction created | No |
| `400` | Missing/invalid idempotency key or malformed request | No; fix request |
| `401` | Missing, invalid, disabled, or rotated API key | No; fix credentials |
| `409` | Event already processing, key reused, or dependency not created yet | Yes for processing/dependency; no for changed payload |
| `422` | Validation, currency, amount, or account mismatch | No; fix data |
| `500` | Unexpected server failure | Yes |
| `502`/`503` | Temporary upstream or service failure | Yes |

Recommended retry policy for retryable responses and network timeouts:

```text
Attempt 1: immediately
Attempt 2: after 2 seconds
Attempt 3: after 10 seconds
Attempt 4: after 60 seconds
Attempt 5: after 5 minutes
Then place the event in a dead-letter queue and alert the product team.
```

Keep the same idempotency key and exact body for every retry.

## 12. Implementation examples

### Node.js / TypeScript

```ts
const payload = {
  externalInvoiceId: stripeInvoice.id,
  invoicedAt: new Date(stripeInvoice.created * 1000).toISOString(),
  paidAt: new Date().toISOString(),
  currency: stripeInvoice.currency.toUpperCase(),
  customer: {
    externalId: stripeCustomer.id,
    name: stripeCustomer.name || stripeCustomer.email || "Customer",
    email: stripeCustomer.email || undefined,
  },
  items: [{
    externalProductId: "monthly-plan",
    description: "Monthly subscription",
    quantity: 1,
    unitAmount: 79.99,
    taxPercent: 0,
  }],
  payment: {
    provider: "stripe",
    method: "card",
    accountId: process.env.STRIPE_ACCOUNT_ID,
    paymentId: paymentIntent.id,
    chargeId: charge.id,
  },
};

const response = await fetch("https://api.cozzix.com/api/external/v1/invoices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.COZZIX_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": stripeEvent.id,
  },
  body: JSON.stringify(payload),
});

const result = await response.json();
if (!response.ok) throw new Error(`Cozzix ${response.status}: ${result.error}`);
```

### PHP

```php
<?php
$payload = [
    'externalInvoiceId' => $stripeInvoiceId,
    'paidAt' => gmdate('c'),
    'currency' => 'USD',
    'customer' => [
        'externalId' => $customerId,
        'name' => $customerName,
        'email' => $customerEmail,
    ],
    'items' => [[
        'description' => 'Monthly subscription',
        'quantity' => 1,
        'unitAmount' => 79.99,
        'taxPercent' => 0,
    ]],
    'payment' => [
        'provider' => 'stripe',
        'method' => 'card',
        'accountId' => getenv('STRIPE_ACCOUNT_ID'),
        'paymentId' => $paymentIntentId,
        'chargeId' => $chargeId,
    ],
];

$ch = curl_init('https://api.cozzix.com/api/external/v1/invoices');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('COZZIX_API_KEY'),
        'Content-Type: application/json',
        'Idempotency-Key: ' . $stripeEventId,
    ],
    CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_SLASHES),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status < 200 || $status >= 300) {
    throw new RuntimeException("Cozzix API error {$status}: {$body}");
}
```

## 13. Go-live checklist

- [ ] A dedicated Business Line and API key were created for this product.
- [ ] `GET /status` returns the expected Business Line and Stripe account.
- [ ] The product's currency appears in the status response.
- [ ] The API key is stored only in production secrets.
- [ ] Test mode and live mode never share an idempotency namespace.
- [ ] A paid invoice test creates exactly one client, invoice, payment, and transaction.
- [ ] Replaying the same event does not create duplicates.
- [ ] A failed payment appears as `failed` without creating a paid invoice.
- [ ] A partial refund creates a linked credit note and refund.
- [ ] Logs redact Authorization headers and payment details.
- [ ] Retry and dead-letter handling are enabled.
- [ ] Alerts exist for repeated `401`, `409`, `422`, and `5xx` responses.

## 14. Support handoff information

When reporting an integration issue, include:

- Business Line code
- Endpoint
- UTC timestamp
- Idempotency key / Stripe Event ID
- Stripe account ID
- PaymentIntent and Charge IDs
- HTTP status
- Response body with secrets removed

Never send the API key, full Authorization header, full card number, or CVC in a support request.

## 15. Versioning

The current API version is `v1` and is included in the URL. Backward-compatible fields may be added without changing the version. Breaking request or response changes will use a new versioned path and a documented migration period.

