# PropFirmConnector License and Download API

Server-to-server license management and addon downloads for approved prop firms. Use one production Vendor Admin key with the permissions required by each endpoint. Keep the key only in your server secret manager; never expose it in browser code, customer URLs, analytics, or logs. The credential determines the vendor and environment. Request bodies and query strings cannot select either.

Onboarding determines whether your integration uses monthly/lifetime resale access or sponsored access, which plans are allowed, and applicable limits. A key cannot switch its approved mode by changing a request. Resale requests require plan on every PUT; sponsored-access requests must omit plan. Lifetime access is available only when explicitly approved. Commercial terms are agreed separately.

Examples are illustrative and contain no usable credentials or download links. There is no public sandbox. Develop against local mocked responses and coordinate the first live activation; production requests create real licenses and applicable charges. See [launch requirements](https://propfirmconnector.com/vendor-api/licensing/#launch).

Canonical guide: https://propfirmconnector.com/vendor-api/licensing/
OpenAPI 3.1.0, contract version 1.1.0: https://propfirmconnector.com/vendor-api/licensing/openapi.json

This Markdown reference is generated from the public OpenAPI contract at each site build. Schema references in this document resolve against that OpenAPI document. The HTML guide also includes integration walkthroughs and onboarding information.

Base URL: `https://propfirmconnector.com`

## Authentication

### VendorBearerAuth

Production Vendor Admin API key. Use Authorization: Bearer <your server-side key>. Choose the key type in Vendor Admin when you create the key: License management and downloads (sponsored_licenses:write, sponsored_licenses:read, addon:download) when one server does both; License management (sponsored_licenses:write, sponsored_licenses:read) when another server requests downloads; Addon download only (addon:download) for a delivery server that must not change licenses; Audit log (read only) (audit_events:read) for a monitoring tool. A Discount codes key belongs to the Coupon API and cannot call these endpoints. Vendor Admin lists only the types your organization is approved for, and a key keeps its type.

A new key must make its first authenticated API request within 24 hours or it expires. With sponsored_licenses:read, GET an unused valid customer ID: 404 SPONSORED_LICENSE_NOT_FOUND confirms authentication and activates the key without issuing a license. Rotation preserves existing permissions; deploy and verify the replacement before revoking the old key. The portal permits at most two non-expired pending or active keys per environment, with at most one pending. See [credential setup](https://propfirmconnector.com/vendor-api/licensing/#credentials).

Missing, invalid, expired, revoked, wrong-environment, or insufficient-scope credentials intentionally share the same 401 response.

Security scheme: `http`; scheme: `bearer`.

## Endpoints

| Method | Path | Purpose |
| --- | --- | --- |
| PUT | `/vendor/v1/sponsored-licenses/{external_customer_id}` | Enable, disable, or upgrade a customer license |
| GET | `/vendor/v1/sponsored-licenses/{external_customer_id}` | Read the current customer license state |
| POST | `/vendor/v1/addon-downloads` | Create a short-lived addon download link |
| GET | `/vendor/v1/audit-events` | Read your organization’s audit log |

## PUT /vendor/v1/sponsored-licenses/{external_customer_id}

Enable, disable, or upgrade a customer license

Requires sponsored_licenses:write and approved direct license access. Your configured mode determines the request shape: monthly/lifetime resale access requires plan on every PUT; sponsored access rejects plan and uses the approved sponsorship automatically. Use only the mode and plans enabled during onboarding.

For a new email, PropFirmConnector creates an account; for an existing account it adds a separate license slot and preserves independently purchased licenses. You decide which email holds a monthly or sponsored license: a higher revision with a different email moves it. previous_email is an optional guard. The same request may also change active, but not plan. Responses set email_changed=true on the revision that moved it. The license, its revision history and its billing move to the account for the new email, which is created if needed. The previous account loses only this license and its email may be provisioned again under another customer ID. Nothing is charged. The customer signs in with the new email and activates their device again. There is one slot per vendor and customer account. A new customer identifier for an email whose license from you is active is refused with SPONSORED_LICENSE_EMAIL_IN_USE before anything is recorded. If that license is cancelled, the new identifier takes the account over and is billed as a new license; reuse the original identifier to keep paid coverage. Monthly and sponsored access stay enabled until explicitly disabled with active=false and a higher revision. Approved lifetime access permits active=true only and displays as ordinary Lifetime access. An existing monthly slot may be upgraded to lifetime using a higher revision, if approved; this converts the slot instead of adding another slot. Lifetime cannot be disabled, changed back to monthly, or moved to another email. This API does not send an onboarding email or return a license key.

Send Content-Type: application/json (optionally charset=utf-8). The body must be JSON, at most 4096 UTF-8 bytes, with no extra fields or query parameters. Retain each outgoing change durably with one idempotency key. After a timeout or transient failure, retry the exact same body, revision, and key. A successful retry returns current state. A stale update normally returns current state with stale=true, but any attempt to disable, downgrade or re-email lifetime is rejected. For status=pending, inspect provisioning.state and provisioning.retryable. A blocked result requires operator intervention: stop automatic retries and preserve the original customer ID and outgoing job. For a retryable pending result, retry the original PUT with the same body, revision and idempotency key using bounded backoff. GET is read-only and cannot finish provisioning. active is desired state, not evidence of a completed grant. Direct-license changes do not emit lifecycle webhooks. Reconcile using durable outgoing jobs and GET; coupon webhooks do not report these changes. See [retry rules](https://propfirmconnector.com/vendor-api/licensing/#retries).

Monthly resale accrues a charge every 30 days while enabled, starting with successful activation; no renewal PUT is required. Disabling stops future renewals without a prorated credit. Reactivation keeps unexpired paid coverage and its renewal date; after expiry it starts a new paid period, without billing disabled time. Lifetime is charged once at the agreed lifetime rate. An upgrade charges that full agreed lifetime rate and stops monthly renewals, without automatic credit for past monthly charges. Reads, downloads and safe retries have no separate license charge. Rates and sponsored-access terms are agreed during onboarding. See [billing rules](https://propfirmconnector.com/vendor-api/licensing/#billing).

Current default limits: 30 writes per vendor per fixed minute, shared across its keys, plus 100 first-activation reservations per UTC day. An email change uses one only when the new email needs a new account. Failed account creation consumes its reservation; retries and later reactivations reuse it. Confirm limits during onboarding and honor Retry-After.

Every response includes X-Correlation-ID, a server-generated UUID for support and request tracing. Retain it with the HTTP status and error code; never log bearer secrets or signed download URLs.

Required permissions: `sponsored_licenses:write`.

Maximum request body: 4096 UTF-8 bytes.

### Parameters

| Name | Location | Required | Description | Schema |
| --- | --- | --- | --- | --- |
| `external_customer_id` | path | Yes | Your stable customer identifier. Reuse it across monthly changes, reactivation, and a lifetime upgrade. Do not use an email address or a per-purchase identifier. Case-insensitive; responses return it in lowercase. No query parameters are accepted. | [ExternalCustomerId](#schema-externalcustomerid) |
| `Idempotency-Key` | header | Yes | A stable random identifier for one outgoing change. Generate once and reuse unchanged for retries. Must be 16–128 ASCII characters, start with a letter or digit, and otherwise contain only letters, digits, dot, underscore, colon, or hyphen. Do not encode customer information. A UUID is suitable. Idempotency records are retained for 90 days; customer bindings and revision ordering remain durable. Expiry does not permit reusing a customer identity or resetting revisions. | `{"type":"string","minLength":16,"maxLength":128,"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{15,127}$"}` |

### Request body (application/json)

Required: yes. Schema: [LicenseWriteRequest](#schema-licensewriterequest)

Enable monthly access (approved resale mode example):

```json
{
  "email": "customer@example.com",
  "plan": "monthly",
  "active": true,
  "revision": 1
}
```

Disable monthly access (approved resale mode example):

```json
{
  "email": "customer@example.com",
  "plan": "monthly",
  "active": false,
  "revision": 2
}
```

Change a monthly customer's email (approved resale mode example):

```json
{
  "email": "new-address@example.com",
  "plan": "monthly",
  "active": true,
  "revision": 3
}
```

Change a monthly customer's email only if the email on record is the expected one:

```json
{
  "email": "new-address@example.com",
  "previous_email": "customer@example.com",
  "plan": "monthly",
  "active": true,
  "revision": 3
}
```

Enable lifetime access (approved resale mode example):

```json
{
  "email": "customer@example.com",
  "plan": "lifetime",
  "active": true,
  "revision": 1
}
```

Upgrade an existing monthly license (approved resale mode example):

```json
{
  "email": "customer@example.com",
  "plan": "lifetime",
  "active": true,
  "revision": 3
}
```

Enable access (approved sponsored mode example):

```json
{
  "email": "customer@example.com",
  "active": true,
  "revision": 1
}
```

Disable access (approved sponsored mode example):

```json
{
  "email": "customer@example.com",
  "active": false,
  "revision": 2
}
```

Change a sponsored customer's email (approved sponsored mode example):

```json
{
  "email": "new-address@example.com",
  "active": true,
  "revision": 3
}
```

### Responses

#### HTTP 200

Current license state. For status=pending, inspect provisioning.state and provisioning.retryable. A blocked result requires operator intervention: stop automatic retries and preserve the original customer ID and outgoing job. For a retryable pending result, retry the original PUT with the same body, revision and idempotency key using bounded backoff. GET is read-only and cannot finish provisioning. active is desired state, not evidence of a completed grant.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [LicenseWriteResponse](#schema-licensewriteresponse)

Approved monthly resale access (example):

```json
{
  "id": "slic_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "external_customer_id": "customer_7b8a1f3d_42e6_4cb9_a2f1",
  "active": true,
  "plan": "monthly",
  "revision": 1,
  "status": "active",
  "email_changed": false,
  "account_url": "https://propfirmconnector.com/account",
  "replayed": false,
  "stale": false,
  "provisioning": {
    "state": "ready",
    "error_code": null,
    "retryable": false
  }
}
```

Approved sponsored access (example):

```json
{
  "id": "slic_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "external_customer_id": "customer_7b8a1f3d_42e6_4cb9_a2f1",
  "active": true,
  "revision": 1,
  "status": "active",
  "email_changed": false,
  "account_url": "https://propfirmconnector.com/account",
  "replayed": false,
  "stale": false,
  "provisioning": {
    "state": "ready",
    "error_code": null,
    "retryable": false
  }
}
```

#### HTTP 400

Invalid path, headers, body, encoding, or query parameters. Check the strict request schema and immutable customer mapping.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "The sponsored license request is invalid.",
  "code": "SPONSORED_LICENSE_INVALID_REQUEST"
}
```

#### HTTP 401

Missing, invalid, expired, revoked, wrong-environment, or insufficient-scope API key. Check the key and its permissions; these cases intentionally return the same response.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Vendor authentication failed.",
  "code": "VENDOR_AUTHENTICATION_FAILED"
}
```

#### HTTP 403

Direct license access is not permitted for this vendor.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Sponsored license access is not authorized.",
  "code": "SPONSORED_LICENSE_FORBIDDEN"
}
```

#### HTTP 404

previous_email was sent for a customer identifier that has no license mapping. There is no email to change; provision the customer without previous_email.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Sponsored license was not found.",
  "code": "SPONSORED_LICENSE_NOT_FOUND"
}
```

#### HTTP 405

Unsupported HTTP method. The error response also includes allowed.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Method not allowed.",
  "code": "METHOD_NOT_ALLOWED",
  "allowed": "PUT, GET"
}
```

#### HTTP 409

Do not automatically retry without understanding the conflict. Codes: SPONSORED_LICENSE_IDENTITY_CONFLICT (account records for that email need support review), SPONSORED_LICENSE_IDEMPOTENCY_CONFLICT (key reused for different content), SPONSORED_LICENSE_REVISION_CONFLICT (same revision, different state), SPONSORED_LICENSE_LIFETIME_PERMANENT (lifetime disable, downgrade or email change), SPONSORED_LICENSE_PREVIOUS_EMAIL_MISMATCH (the optional previous_email guard is not the email on record; nothing moved), SPONSORED_LICENSE_EMAIL_IN_USE (that email has an active license from your firm under the customer ID in conflicting_external_customer_id; nothing was recorded), SPONSORED_LICENSE_LEGACY_REVIEW_REQUIRED (existing access needs support review), SPONSORED_ENTITLEMENT_CONFLICT (conflicting customer grants). Contact PropFirmConnector support for identity or existing-access conflicts.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Lifetime licenses cannot be disabled or changed to monthly.",
  "code": "SPONSORED_LICENSE_LIFETIME_PERMANENT"
}
```

#### HTTP 413

Request exceeds the endpoint byte limit.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Request body is too large.",
  "code": "VENDOR_REQUEST_TOO_LARGE"
}
```

#### HTTP 415

Content-Type must be application/json, optionally with charset=utf-8.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Content-Type must be application/json.",
  "code": "SPONSORED_LICENSE_INVALID_REQUEST"
}
```

#### HTTP 429

Codes: VENDOR_RATE_LIMITED (minute limit) or SPONSORED_LICENSE_DAILY_LIMIT (first-activation daily limit). Honor Retry-After: 60 seconds for the minute limit, or seconds until the next UTC day for the daily limit. Retry the same outgoing change without changing its idempotency key or revision.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `Retry-After` | `^[0-9]+$` | When present, seconds to wait before retrying. Honor this value before applying backoff. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Too many vendor requests. Please try again later.",
  "code": "VENDOR_RATE_LIMITED"
}
```

#### HTTP 500

Unexpected service failure. The desired change may already have been accepted. Retain the outgoing job, retry the exact same PUT with bounded backoff, and reconcile through GET.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Something went wrong on our end. Please try again in a moment.",
  "code": "SERVICE_UNAVAILABLE"
}
```

#### HTTP 503

Temporary service or reconciliation failure. Codes include SPONSORED_LICENSE_UNAVAILABLE, SPONSORED_LICENSE_PROJECTION_PENDING, SPONSORED_LICENSE_BILLING_PENDING, and VENDOR_PROMOTION_SERVICE_UNAVAILABLE. Retry the same PUT with bounded exponential backoff and jitter. SPONSORED_LICENSE_BILLING_PENDING includes Retry-After: 60. Do not replace the customer identity, revision, or idempotency key simply to bypass a pending operation.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `Retry-After` | `^[0-9]+$` | When present, seconds to wait before retrying. Honor this value before applying backoff. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "A newer license update is being applied. Retry this request.",
  "code": "SPONSORED_LICENSE_PROJECTION_PENDING"
}
```

## GET /vendor/v1/sponsored-licenses/{external_customer_id}

Read the current customer license state

Requires sponsored_licenses:read and approved direct license access. Returns only the license associated with this vendor and customer identifier. Does not return the customer email, license key, unrelated licenses, or payment history. Do not send a request body or query parameters. Idempotency-Key is not required. Resale responses include plan; sponsored-access responses omit it. GET does not retry or repair a pending change; retry the original PUT if necessary. Current default limit: 120 reads per vendor per fixed minute, shared across its keys. Confirm limits during onboarding and honor Retry-After.

For status=pending, inspect provisioning.state and provisioning.retryable. A blocked result requires operator intervention: stop automatic retries and preserve the original customer ID and outgoing job. For a retryable pending result, retry the original PUT with the same body, revision and idempotency key using bounded backoff. GET is read-only and cannot finish provisioning. active is desired state, not evidence of a completed grant.

Every response includes X-Correlation-ID, a server-generated UUID for support and request tracing. Retain it with the HTTP status and error code; never log bearer secrets or signed download URLs.

Required permissions: `sponsored_licenses:read`.

### Parameters

| Name | Location | Required | Description | Schema |
| --- | --- | --- | --- | --- |
| `external_customer_id` | path | Yes | Your stable customer identifier. Reuse it across monthly changes, reactivation, and a lifetime upgrade. Do not use an email address or a per-purchase identifier. Case-insensitive; responses return it in lowercase. No query parameters are accepted. | [ExternalCustomerId](#schema-externalcustomerid) |

### Responses

#### HTTP 200

Current desired and applied state.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [LicenseState](#schema-licensestate)

Approved monthly resale access (example):

```json
{
  "id": "slic_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "external_customer_id": "customer_7b8a1f3d_42e6_4cb9_a2f1",
  "active": true,
  "plan": "monthly",
  "revision": 1,
  "status": "active",
  "email_changed": false,
  "account_url": "https://propfirmconnector.com/account",
  "provisioning": {
    "state": "ready",
    "error_code": null,
    "retryable": false
  }
}
```

Approved sponsored access (example):

```json
{
  "id": "slic_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "external_customer_id": "customer_7b8a1f3d_42e6_4cb9_a2f1",
  "active": true,
  "revision": 1,
  "status": "active",
  "email_changed": false,
  "account_url": "https://propfirmconnector.com/account",
  "provisioning": {
    "state": "ready",
    "error_code": null,
    "retryable": false
  }
}
```

#### HTTP 400

Invalid path, headers, body, encoding, or query parameters. Check the strict request schema and immutable customer mapping.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "The sponsored license request is invalid.",
  "code": "SPONSORED_LICENSE_INVALID_REQUEST"
}
```

#### HTTP 401

Missing, invalid, expired, revoked, wrong-environment, or insufficient-scope API key. Check the key and its permissions; these cases intentionally return the same response.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Vendor authentication failed.",
  "code": "VENDOR_AUTHENTICATION_FAILED"
}
```

#### HTTP 403

Direct license access is not permitted for this vendor.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Sponsored license access is not authorized.",
  "code": "SPONSORED_LICENSE_FORBIDDEN"
}
```

#### HTTP 404

No license mapping exists for this customer identifier under this vendor.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Sponsored license was not found.",
  "code": "SPONSORED_LICENSE_NOT_FOUND"
}
```

#### HTTP 405

Unsupported HTTP method. The error response also includes allowed.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Method not allowed.",
  "code": "METHOD_NOT_ALLOWED",
  "allowed": "PUT, GET"
}
```

#### HTTP 409

Do not automatically retry without understanding the conflict. Codes: SPONSORED_LICENSE_IDENTITY_CONFLICT (account records for that email need support review), SPONSORED_LICENSE_IDEMPOTENCY_CONFLICT (key reused for different content), SPONSORED_LICENSE_REVISION_CONFLICT (same revision, different state), SPONSORED_LICENSE_LIFETIME_PERMANENT (lifetime disable, downgrade or email change), SPONSORED_LICENSE_PREVIOUS_EMAIL_MISMATCH (the optional previous_email guard is not the email on record; nothing moved), SPONSORED_LICENSE_EMAIL_IN_USE (that email has an active license from your firm under the customer ID in conflicting_external_customer_id; nothing was recorded), SPONSORED_LICENSE_LEGACY_REVIEW_REQUIRED (existing access needs support review), SPONSORED_ENTITLEMENT_CONFLICT (conflicting customer grants). Contact PropFirmConnector support for identity or existing-access conflicts.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Lifetime licenses cannot be disabled or changed to monthly.",
  "code": "SPONSORED_LICENSE_LIFETIME_PERMANENT"
}
```

#### HTTP 413

Request exceeds the endpoint byte limit.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Request body is too large.",
  "code": "VENDOR_REQUEST_TOO_LARGE"
}
```

#### HTTP 429

Codes: VENDOR_RATE_LIMITED (minute limit) or SPONSORED_LICENSE_DAILY_LIMIT (first-activation daily limit). Honor Retry-After: 60 seconds for the minute limit, or seconds until the next UTC day for the daily limit. Retry the same outgoing change without changing its idempotency key or revision.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `Retry-After` | `^[0-9]+$` | When present, seconds to wait before retrying. Honor this value before applying backoff. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Too many vendor requests. Please try again later.",
  "code": "VENDOR_RATE_LIMITED"
}
```

#### HTTP 500

Unexpected service failure. The desired change may already have been accepted. Retain the outgoing job, retry the exact same PUT with bounded backoff, and reconcile through GET.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Something went wrong on our end. Please try again in a moment.",
  "code": "SERVICE_UNAVAILABLE"
}
```

#### HTTP 503

Temporary service or reconciliation failure. Codes include SPONSORED_LICENSE_UNAVAILABLE, SPONSORED_LICENSE_PROJECTION_PENDING, SPONSORED_LICENSE_BILLING_PENDING, and VENDOR_PROMOTION_SERVICE_UNAVAILABLE. Retry the same PUT with bounded exponential backoff and jitter. SPONSORED_LICENSE_BILLING_PENDING includes Retry-After: 60. Do not replace the customer identity, revision, or idempotency key simply to bypass a pending operation.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `Retry-After` | `^[0-9]+$` | When present, seconds to wait before retrying. Honor this value before applying backoff. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "A newer license update is being applied. Retry this request.",
  "code": "SPONSORED_LICENSE_PROJECTION_PENDING"
}
```

## POST /vendor/v1/addon-downloads

Create a short-lived addon download link

Requires addon:download and approved addon download access. Your authenticated server route must derive external_customer_id from the signed-in customer. Do not trust a browser-supplied customer identifier or email. Request a link only after a deliberate download click. The exact license issued by this vendor must be active and fully applied; another purchased license alone does not qualify. This endpoint never provisions or repairs a license. Send Content-Type: application/json (optionally charset=utf-8), with at most 1024 UTF-8 bytes and no extra fields or query parameters. No Idempotency-Key is required. Each successful call returns a new five-minute bearer link to the currently promoted production ZIP. The link may be reused until expiration. Do not persist, email, log, or send it to analytics. Your server may redirect the customer to the returned URL with HTTP 303, Cache-Control: no-store and Referrer-Policy: no-referrer. Current default limits: 120 requests per vendor per fixed minute and 30 per customer per fixed hour, shared across keys. A download attempt can consume its limit even if a later step fails. Confirm limits during onboarding and honor Retry-After.

Every response includes X-Correlation-ID, a server-generated UUID for support and request tracing. Retain it with the HTTP status and error code; never log bearer secrets or signed download URLs.

Required permissions: `addon:download`.

Maximum request body: 1024 UTF-8 bytes.

Download URL lifetime: 300 seconds.

### Request body (application/json)

Required: yes. Schema: [DownloadRequest](#schema-downloadrequest)

Example:

```json
{
  "external_customer_id": "customer_7b8a1f3d_42e6_4cb9_a2f1"
}
```

### Responses

#### HTTP 200

A temporary link and metadata for the currently promoted production addon. Example version, release, hashes and URL are illustrative only.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `Referrer-Policy` | `no-referrer` |  |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [DownloadResponse](#schema-downloadresponse)

Example:

```json
{
  "url": "https://storage.googleapis.com/example-release/PropFirmConnector.zip?example=not-a-valid-download-link",
  "file_name": "PropFirmConnector.zip",
  "release_id": "pfc-1.2.3-20300101-a1b2c3d4",
  "version": "1.2.3",
  "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "size_bytes": 1048576,
  "expires_at": "2030-01-01T00:05:00.000Z"
}
```

#### HTTP 400

Malformed path, body, encoding, fields, or query parameters.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `Referrer-Policy` | `no-referrer` |  |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "The download request is invalid.",
  "code": "ADDON_DOWNLOAD_INVALID_REQUEST"
}
```

#### HTTP 401

Missing, invalid, expired, revoked, wrong-environment, or insufficient-scope API key. Check the key and its permissions; these cases intentionally return the same response.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `Referrer-Policy` | `no-referrer` |  |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Vendor authentication failed.",
  "code": "VENDOR_AUTHENTICATION_FAILED"
}
```

#### HTTP 403

The required vendor-issued license or identity is unavailable, or the vendor is not permitted to download. Do not provision a replacement license to bypass this error.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `Referrer-Policy` | `no-referrer` |  |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "An active license issued by this vendor is required to download the addon.",
  "code": "ADDON_DOWNLOAD_FORBIDDEN"
}
```

#### HTTP 405

Unsupported HTTP method. The error response also includes allowed.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `Referrer-Policy` | `no-referrer` |  |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Method not allowed.",
  "code": "METHOD_NOT_ALLOWED",
  "allowed": "POST"
}
```

#### HTTP 409

The license is still being applied, or changed while the link was being prepared. Retry after a short delay; reconcile the original provisioning request if needed.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `Referrer-Policy` | `no-referrer` |  |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "The license is still being prepared. Please try again shortly.",
  "code": "ADDON_DOWNLOAD_PENDING"
}
```

#### HTTP 413

Request exceeds the endpoint byte limit.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `Referrer-Policy` | `no-referrer` |  |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Request body is too large.",
  "code": "VENDOR_REQUEST_TOO_LARGE"
}
```

#### HTTP 415

Content-Type must be application/json, optionally with charset=utf-8.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `Referrer-Policy` | `no-referrer` |  |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Content-Type must be application/json.",
  "code": "ADDON_DOWNLOAD_INVALID_REQUEST"
}
```

#### HTTP 429

Vendor or customer download limit reached. Codes: VENDOR_RATE_LIMITED or ADDON_DOWNLOAD_RATE_LIMITED. Honor Retry-After; it is 60 seconds for the vendor limit and up to 3600 seconds for the customer limit.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `Retry-After` | `^[0-9]+$` | When present, seconds to wait before retrying. Honor this value before applying backoff. |
| `Referrer-Policy` | `no-referrer` |  |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Too many addon download requests. Please try again later.",
  "code": "ADDON_DOWNLOAD_RATE_LIMITED"
}
```

#### HTTP 503

Temporary download, authentication-service, or configuration failure. Retry with bounded exponential backoff and jitter. Codes include ADDON_DOWNLOAD_UNAVAILABLE and VENDOR_PROMOTION_SERVICE_UNAVAILABLE.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `Referrer-Policy` | `no-referrer` |  |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "The download is temporarily unavailable. Please try again later.",
  "code": "ADDON_DOWNLOAD_UNAVAILABLE"
}
```

## GET /vendor/v1/audit-events

Read your organization’s audit log

Requires audit_events:read, which only a key created with the Vendor Admin purpose "Audit log (read only)" has; license, download and discount-code keys cannot read the log because it names your administrators. Returns the same events as the Audit log page of Vendor Admin, newest first: every authenticated API request, every portal change, and changes PropFirmConnector support made to administrator access, for 90 days. Events contain no customer email, customer identifier or request body. external_customer_id finds one customer’s events through a stored hash and may be combined only with from and to. event_id returns one event and may not be combined with anything. To continue, send page.next_cursor unchanged with the same filters. Reading the log is itself an audited request. Current default limit: 60 reads per vendor per fixed minute, shared across its keys.

Every response includes X-Correlation-ID.

Required permissions: `audit_events:read`.

### Parameters

| Name | Location | Required | Description | Schema |
| --- | --- | --- | --- | --- |
| `limit` | query | No | Events per page. | `{"type":"integer","minimum":1,"maximum":100,"default":50}` |
| `cursor` | query | No | page.next_cursor from the previous page, with the same filters. | `{"type":"string","maxLength":1024}` |
| `category` | query | No | api, portal, or one event group such as api.sponsored_license_sync, api.authentication_failed or portal.api_keys. | `{"type":"string"}` |
| `outcome` | query | No | success, failed (rejected or failed), or error (server errors and unfinished requests). | `{"type":"string","enum":["success","failed","error"]}` |
| `from` | query | No | Earliest event time, UTC. | `{"type":"string","format":"date-time","example":"2026-09-01T00:00:00Z"}` |
| `to` | query | No | Latest event time, UTC. | `{"type":"string","format":"date-time"}` |
| `key_id` | query | No | Only requests made with this API key ID. | `{"type":"string","pattern":"^k[a-f0-9]{24}$"}` |
| `external_customer_id` | query | No | Only events for this exact customer ID. Combine only with from and to. | [ExternalCustomerId](#schema-externalcustomerid) |
| `event_id` | query | No | One event by its ID or X-Correlation-ID. Cannot be combined with other parameters. | `{"type":"string"}` |

### Responses

#### HTTP 200

One page of events, newest first.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [AuditEventPage](#schema-auditeventpage)

Example:

```json
{
  "data": [
    {
      "id": "7f0c2a4e-1b3d-4c5e-8f60-123456789abc",
      "source": "api",
      "type": "api.sponsored_license_sync",
      "title": "License update",
      "summary": "PUT /vendor/v1/sponsored-licenses/{external_customer_id}",
      "occurred_at": "2026-09-19T14:03:11.482Z",
      "outcome": "failed",
      "actor": {
        "type": "api_key",
        "key_id": "k0123456789abcdef01234567",
        "label": "Production server",
        "last_four": "Zx9Q",
        "status": "revoked"
      },
      "http_status": 401,
      "error_code": "VENDOR_AUTHENTICATION_FAILED",
      "failure_reason": "key_revoked",
      "source_ip": "203.0.113.7"
    }
  ],
  "page": {
    "limit": 50,
    "has_more": false,
    "next_cursor": null
  }
}
```

#### HTTP 400

The query is invalid, or the cursor was issued for different filters.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "The audit events query is invalid.",
  "code": "AUDIT_EVENTS_INVALID_REQUEST"
}
```

#### HTTP 401

Missing, invalid, expired, revoked, wrong-environment, or insufficient-scope API key. Check the key and its permissions; these cases intentionally return the same response.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Vendor authentication failed.",
  "code": "VENDOR_AUTHENTICATION_FAILED"
}
```

#### HTTP 404

No event has that event_id.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "No event with that ID was found.",
  "code": "NOT_FOUND"
}
```

#### HTTP 405

Unsupported HTTP method. The error response also includes allowed.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Method not allowed.",
  "code": "METHOD_NOT_ALLOWED",
  "allowed": "PUT, GET"
}
```

#### HTTP 429

Too many requests. Honor Retry-After.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Too many vendor requests. Please try again later.",
  "code": "VENDOR_RATE_LIMITED"
}
```

#### HTTP 503

Temporarily unavailable. Retry with bounded backoff.

| Response header | Value / constraint | Description |
| --- | --- | --- |
| `Cache-Control` | `no-store` | Responses must not be cached. |
| `X-Correlation-ID` | string / uuid | Server-generated request UUID. Include this value when reporting a failure. It is not an API key or customer identifier. |

Content-Type: `application/json`. Schema: [ApiError](#schema-apierror)

Example:

```json
{
  "error": "Audit events are temporarily unavailable. Retry this request.",
  "code": "AUDIT_EVENTS_UNAVAILABLE"
}
```

## Webhooks

Optional. PropFirmConnector sends these to the HTTPS endpoint configured on the Webhooks page of Vendor Admin. Endpoint rules, signature verification, retries and replay are documented in the [Coupon API reference](https://propfirmconnector.com/vendor-api/reference/#webhooks) and are identical here.

| Event | Sent when |
| --- | --- |
| `vendor.license.cancelled` | A license was cancelled in Vendor Admin |
| `vendor.license.email_changed` | A license email was changed in Vendor Admin |
| `vendor.license.provisioning_blocked` | A license became blocked |

### vendor.license.cancelled

An administrator cancelled a monthly or sponsored license on the Usage page. The cancellation used the next revision. Make the same change in your system: your next PUT states email and active again.

Schema: [LicenseWebhookEvent](#schema-licensewebhookevent)

event:

```json
{
  "id": "7d3f0a52-6c1e-4b8a-9f27-2f4f6f0c9a11",
  "type": "vendor.license.cancelled",
  "api_version": "v1",
  "created_at": "2026-09-19T16:04:12.000Z",
  "environment": "live",
  "vendor_id": "your-vendor-id",
  "data": {
    "source": "vendor_admin",
    "revision": 8,
    "license": {
      "id": "slic_4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f",
      "external_customer_id": "customer-1842",
      "email": "customer@example.com",
      "active": false,
      "plan": "monthly",
      "revision": 8
    }
  }
}
```

### vendor.license.email_changed

An administrator moved a license to another email on the Usage page. data.license.email is the new email. Record it, because your next PUT states the email again and the license follows it.

Schema: [LicenseWebhookEvent](#schema-licensewebhookevent)

event:

```json
{
  "id": "7d3f0a52-6c1e-4b8a-9f27-2f4f6f0c9a11",
  "type": "vendor.license.email_changed",
  "api_version": "v1",
  "created_at": "2026-09-19T16:04:12.000Z",
  "environment": "live",
  "vendor_id": "your-vendor-id",
  "data": {
    "source": "vendor_admin",
    "revision": 8,
    "license": {
      "id": "slic_4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f",
      "external_customer_id": "customer-1842",
      "email": "new-address@example.com",
      "active": true,
      "plan": "monthly",
      "revision": 8
    }
  }
}
```

### vendor.license.provisioning_blocked

Provisioning stopped with a blocked error code that a retry will not clear. The request that caused it already returned this code; the event reaches your server when nobody was watching that response.

Schema: [LicenseWebhookEvent](#schema-licensewebhookevent)

event:

```json
{
  "id": "7d3f0a52-6c1e-4b8a-9f27-2f4f6f0c9a11",
  "type": "vendor.license.provisioning_blocked",
  "api_version": "v1",
  "created_at": "2026-09-19T16:04:12.000Z",
  "environment": "live",
  "vendor_id": "your-vendor-id",
  "data": {
    "source": "provisioning",
    "revision": 8,
    "error_code": "SPONSORED_LICENSE_EMAIL_IN_USE",
    "license": {
      "id": "slic_4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f",
      "external_customer_id": "customer-1842",
      "email": "customer@example.com",
      "active": true,
      "plan": "monthly",
      "revision": 8
    }
  }
}
```

## Schemas

These are JSON Schema definitions from the public OpenAPI document. Required properties, permitted values, field limits, and additional-property restrictions apply as written.

<a id="schema-externalcustomerid"></a>

### ExternalCustomerId

```json
{
  "type": "string",
  "minLength": 1,
  "maxLength": 128,
  "pattern": "^[A-Za-z0-9_-]{1,128}$",
  "description": "Stable customer identifier from your own account records. Case-insensitive: Trader_01 and trader_01 are the same customer. Responses return it in lowercase.",
  "examples": [
    "customer_7b8a1f3d_42e6_4cb9_a2f1"
  ]
}
```

<a id="schema-revision"></a>

### Revision

```json
{
  "type": "integer",
  "minimum": 1,
  "maximum": 9007199254740991,
  "description": "Monotonically increasing desired-state revision for this customer. Increment for each committed state change. Never reset it or reuse a revision with different state.",
  "examples": [
    1
  ]
}
```

<a id="schema-licensewriterequest"></a>

### LicenseWriteRequest

```json
{
  "description": "The server selects the allowed schema using the authenticated vendor configuration. These alternatives describe separate approved modes; callers cannot choose an unapproved mode or plan.",
  "oneOf": [
    {
      "$ref": "#/components/schemas/ResaleMonthlyLicenseWriteRequest"
    },
    {
      "$ref": "#/components/schemas/ResaleLifetimeLicenseWriteRequest"
    },
    {
      "$ref": "#/components/schemas/SponsoredLicenseWriteRequest"
    }
  ]
}
```

<a id="schema-licensestate"></a>

### LicenseState

```json
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "id",
    "external_customer_id",
    "active",
    "revision",
    "status",
    "email_changed",
    "account_url",
    "provisioning"
  ],
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^slic_[a-f0-9]{64}$",
      "description": "Opaque public API resource identifier. It is not a license key or customer account ID."
    },
    "external_customer_id": {
      "$ref": "#/components/schemas/ExternalCustomerId"
    },
    "active": {
      "type": "boolean",
      "description": "Current desired access state. Use status to confirm whether that state has been applied."
    },
    "plan": {
      "type": "string",
      "enum": [
        "monthly",
        "lifetime"
      ],
      "description": "Present for monthly/lifetime resale access. Omitted for sponsored access. The response never returns plan=sponsored."
    },
    "revision": {
      "$ref": "#/components/schemas/Revision"
    },
    "status": {
      "type": "string",
      "enum": [
        "active",
        "inactive",
        "pending"
      ],
      "description": "Applied active/inactive state, or pending when the desired revision is not applied. Inspect provisioning to distinguish a retryable delay from a blocked request."
    },
    "email_changed": {
      "type": "boolean",
      "description": "True while the current revision is the one that moved the license to a different email. Check it to notice a move you did not intend."
    },
    "account_url": {
      "type": "string",
      "format": "uri",
      "const": "https://propfirmconnector.com/account",
      "description": "Send the customer to this account page to sign in with the same email supplied by your firm and complete email verification. After installing the addon, they use its Activate this device browser flow; no license-key entry is required. Your website or confirmation email must supply this link because the API sends no onboarding email. See https://propfirmconnector.com/vendor-api/licensing/#customer-identity."
    },
    "provisioning": {
      "$ref": "#/components/schemas/ProvisioningState"
    }
  }
}
```

<a id="schema-licensewriteresponse"></a>

### LicenseWriteResponse

```json
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "id",
    "external_customer_id",
    "active",
    "revision",
    "status",
    "email_changed",
    "account_url",
    "replayed",
    "stale",
    "provisioning"
  ],
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^slic_[a-f0-9]{64}$",
      "description": "Opaque public API resource identifier. It is not a license key or customer account ID."
    },
    "external_customer_id": {
      "$ref": "#/components/schemas/ExternalCustomerId"
    },
    "active": {
      "type": "boolean",
      "description": "Current desired access state. Use status to confirm whether that state has been applied."
    },
    "plan": {
      "type": "string",
      "enum": [
        "monthly",
        "lifetime"
      ],
      "description": "Present for monthly/lifetime resale access. Omitted for sponsored access. The response never returns plan=sponsored."
    },
    "revision": {
      "$ref": "#/components/schemas/Revision"
    },
    "status": {
      "type": "string",
      "enum": [
        "active",
        "inactive",
        "pending"
      ],
      "description": "Applied active/inactive state, or pending when the desired revision is not applied. Inspect provisioning to distinguish a retryable delay from a blocked request."
    },
    "email_changed": {
      "type": "boolean",
      "description": "True while the current revision is the one that moved the license to a different email. Check it to notice a move you did not intend."
    },
    "account_url": {
      "type": "string",
      "format": "uri",
      "const": "https://propfirmconnector.com/account",
      "description": "Send the customer to this account page to sign in with the same email supplied by your firm and complete email verification. After installing the addon, they use its Activate this device browser flow; no license-key entry is required. Your website or confirmation email must supply this link because the API sends no onboarding email. See https://propfirmconnector.com/vendor-api/licensing/#customer-identity."
    },
    "replayed": {
      "type": "boolean",
      "description": "The request or revision had already been seen. The response still represents current state."
    },
    "stale": {
      "type": "boolean",
      "description": "A higher revision already exists. This older request did not overwrite it."
    },
    "provisioning": {
      "$ref": "#/components/schemas/ProvisioningState"
    }
  }
}
```

<a id="schema-downloadrequest"></a>

### DownloadRequest

```json
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "external_customer_id"
  ],
  "properties": {
    "external_customer_id": {
      "$ref": "#/components/schemas/ExternalCustomerId"
    }
  }
}
```

<a id="schema-downloadresponse"></a>

### DownloadResponse

```json
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "url",
    "file_name",
    "release_id",
    "version",
    "sha256",
    "size_bytes",
    "expires_at"
  ],
  "properties": {
    "url": {
      "type": "string",
      "format": "uri",
      "pattern": "^https://",
      "description": "Temporary bearer link. Follow it only for the customer who requested it. It expires five minutes after issuance."
    },
    "file_name": {
      "type": "string",
      "const": "PropFirmConnector.zip"
    },
    "release_id": {
      "type": "string",
      "pattern": "^pfc-((?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*))-[0-9]{8}-[a-f0-9]{8}$",
      "description": "Public identifier for the promoted addon release."
    },
    "version": {
      "type": "string",
      "pattern": "^(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)$",
      "description": "Version of the promoted production addon."
    },
    "sha256": {
      "type": "string",
      "pattern": "^[a-f0-9]{64}$",
      "description": "SHA-256 digest of the complete ZIP."
    },
    "size_bytes": {
      "type": "integer",
      "minimum": 1,
      "maximum": 104857600,
      "description": "ZIP size in bytes."
    },
    "expires_at": {
      "type": "string",
      "format": "date-time",
      "description": "UTC expiration time of this link."
    }
  }
}
```

<a id="schema-apierror"></a>

### ApiError

```json
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "error",
    "code"
  ],
  "properties": {
    "error": {
      "type": "string",
      "description": "Readable explanation. Do not parse this text for control flow."
    },
    "code": {
      "type": "string",
      "pattern": "^[A-Z][A-Z0-9_]{2,63}$",
      "description": "Machine-readable error code."
    },
    "allowed": {
      "type": "string",
      "description": "Present on METHOD_NOT_ALLOWED; lists permitted methods."
    },
    "conflicting_external_customer_id": {
      "$ref": "#/components/schemas/ExternalCustomerId",
      "description": "Present on SPONSORED_LICENSE_EMAIL_IN_USE: your customer ID whose active license holds that email."
    }
  }
}
```

<a id="schema-resalemonthlylicensewriterequest"></a>

### ResaleMonthlyLicenseWriteRequest

```json
{
  "type": "object",
  "additionalProperties": false,
  "description": "Available only to integrations approved for monthly resale licenses. This mode requires plan on every PUT, including disable and reactivation.",
  "required": [
    "email",
    "plan",
    "active",
    "revision"
  ],
  "properties": {
    "email": {
      "type": "string",
      "maxLength": 254,
      "format": "email",
      "description": "Customer email from your verified account records. Send the normalized address; the API trims it and converts it to lowercase. Send the email that should hold the license: a newer revision with a different email moves the license to it.",
      "examples": [
        "customer@example.com"
      ]
    },
    "plan": {
      "type": "string",
      "const": "monthly"
    },
    "active": {
      "type": "boolean",
      "description": "Desired monthly access. Stays enabled until explicitly disabled."
    },
    "revision": {
      "$ref": "#/components/schemas/Revision"
    },
    "previous_email": {
      "type": "string",
      "maxLength": 254,
      "format": "email",
      "description": "Optional guard for an email change: the address you believe is on record. If it is not, the request fails with SPONSORED_LICENSE_PREVIOUS_EMAIL_MISMATCH and nothing moves. An email change may also change active, but not plan.",
      "examples": [
        "customer@example.com"
      ]
    }
  }
}
```

<a id="schema-resalelifetimelicensewriterequest"></a>

### ResaleLifetimeLicenseWriteRequest

```json
{
  "type": "object",
  "additionalProperties": false,
  "description": "Available only to integrations approved for lifetime resale licenses. Creates permanent ordinary Lifetime access or upgrades the existing monthly slot; cannot disable or downgrade.",
  "required": [
    "email",
    "plan",
    "active",
    "revision"
  ],
  "properties": {
    "email": {
      "type": "string",
      "maxLength": 254,
      "format": "email",
      "description": "Customer email from your verified account records. Send the normalized address; the API trims it and converts it to lowercase. A lifetime license is locked to this email; it cannot be changed.",
      "examples": [
        "customer@example.com"
      ]
    },
    "plan": {
      "type": "string",
      "const": "lifetime"
    },
    "active": {
      "type": "boolean",
      "const": true,
      "description": "Lifetime access can only be enabled."
    },
    "revision": {
      "$ref": "#/components/schemas/Revision"
    }
  }
}
```

<a id="schema-sponsoredlicensewriterequest"></a>

### SponsoredLicenseWriteRequest

```json
{
  "type": "object",
  "additionalProperties": false,
  "description": "Use only if your integration was approved for sponsored access. Omit plan entirely; specifying it is rejected. Sponsored access is reversible and displays the approved sponsor name in the customer account.",
  "required": [
    "email",
    "active",
    "revision"
  ],
  "properties": {
    "email": {
      "type": "string",
      "maxLength": 254,
      "format": "email",
      "description": "Customer email from your verified account records. Send the normalized address; the API trims it and converts it to lowercase. Send the email that should hold the license: a newer revision with a different email moves the license to it.",
      "examples": [
        "customer@example.com"
      ]
    },
    "active": {
      "type": "boolean",
      "description": "Desired sponsored access. Disable only when none of the customer subscriptions or accounts covered by your integration remain active."
    },
    "revision": {
      "$ref": "#/components/schemas/Revision"
    },
    "previous_email": {
      "type": "string",
      "maxLength": 254,
      "format": "email",
      "description": "Optional guard for an email change: the address you believe is on record. If it is not, the request fails with SPONSORED_LICENSE_PREVIOUS_EMAIL_MISMATCH and nothing moves. An email change may also change active.",
      "examples": [
        "customer@example.com"
      ]
    }
  }
}
```

<a id="schema-provisioningstate"></a>

### ProvisioningState

```json
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "state",
    "error_code",
    "retryable"
  ],
  "description": "For status=pending, inspect provisioning.state and provisioning.retryable. A blocked result requires operator intervention: stop automatic retries and preserve the original customer ID and outgoing job. For a retryable pending result, retry the original PUT with the same body, revision and idempotency key using bounded backoff. GET is read-only and cannot finish provisioning. active is desired state, not evidence of a completed grant.",
  "properties": {
    "state": {
      "type": "string",
      "enum": [
        "ready",
        "pending",
        "blocked"
      ],
      "description": "ready means the current revision is applied; pending can be retried; blocked requires an account or identity correction by an operator."
    },
    "error_code": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        null,
        "SPONSORED_LICENSE_IDENTITY_CONFLICT",
        "SPONSORED_LICENSE_EMAIL_IN_USE",
        "SPONSORED_LICENSE_LEGACY_REVIEW_REQUIRED",
        "SPONSORED_ENTITLEMENT_CONFLICT",
        "SPONSORED_LICENSE_PROJECTION_PENDING",
        "SPONSORED_LICENSE_BILLING_PENDING",
        "SPONSORED_LICENSE_PROVISIONING_PENDING"
      ],
      "description": "Safe reason for the current provisioning delay or block. Null when ready or no failure has been recorded."
    },
    "retryable": {
      "type": "boolean",
      "description": "True only when the pending change can be retried. False for ready and blocked states."
    }
  }
}
```

<a id="schema-auditevent"></a>

### AuditEvent

```json
{
  "type": "object",
  "additionalProperties": true,
  "required": [
    "id",
    "source",
    "type",
    "title",
    "occurred_at",
    "outcome",
    "actor"
  ],
  "properties": {
    "id": {
      "type": "string",
      "description": "For an API request this is the X-Correlation-ID that request returned."
    },
    "source": {
      "type": "string",
      "enum": [
        "api",
        "portal",
        "support"
      ]
    },
    "type": {
      "type": "string",
      "description": "Stable event type, for example api.sponsored_license_sync or portal.api_key.revoked."
    },
    "title": {
      "type": "string"
    },
    "summary": {
      "type": [
        "string",
        "null"
      ],
      "description": "For an API request, the method and documented route. Never a customer identifier."
    },
    "occurred_at": {
      "type": "string",
      "format": "date-time"
    },
    "outcome": {
      "type": "string",
      "enum": [
        "success",
        "failed",
        "error",
        "incomplete"
      ]
    },
    "actor": {
      "type": "object",
      "additionalProperties": true,
      "required": [
        "type"
      ],
      "properties": {
        "type": {
          "type": "string",
          "enum": [
            "api_key",
            "admin",
            "support"
          ]
        },
        "key_id": {
          "type": [
            "string",
            "null"
          ]
        },
        "label": {
          "type": [
            "string",
            "null"
          ]
        },
        "last_four": {
          "type": [
            "string",
            "null"
          ]
        },
        "status": {
          "type": [
            "string",
            "null"
          ]
        },
        "email": {
          "type": [
            "string",
            "null"
          ]
        },
        "role": {
          "type": [
            "string",
            "null"
          ]
        }
      }
    },
    "http_status": {
      "type": [
        "integer",
        "null"
      ]
    },
    "error_code": {
      "type": [
        "string",
        "null"
      ]
    },
    "failure_reason": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "secret_mismatch",
        "key_revoked",
        "key_expired",
        "key_inactive",
        "scope_denied",
        "key_invalid",
        null
      ],
      "description": "Why a request using one of your key IDs was rejected. The rejected caller only ever receives the generic 401."
    },
    "source_ip": {
      "type": [
        "string",
        "null"
      ],
      "description": "The address the request arrived from."
    }
  }
}
```

<a id="schema-auditeventpage"></a>

### AuditEventPage

```json
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "data"
  ],
  "properties": {
    "data": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/AuditEvent"
      }
    },
    "page": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "limit",
        "has_more",
        "next_cursor"
      ],
      "description": "Omitted when event_id is used.",
      "properties": {
        "limit": {
          "type": "integer"
        },
        "has_more": {
          "type": "boolean"
        },
        "next_cursor": {
          "type": [
            "string",
            "null"
          ]
        }
      }
    }
  }
}
```

<a id="schema-licensewebhookevent"></a>

### LicenseWebhookEvent

```json
{
  "type": "object",
  "description": "Signed exactly like Coupon API events: verify X-PFC-Webhook-Signature over the raw body before parsing it. Deduplicate by id. Delivery is at-least-once and may arrive out of order.",
  "additionalProperties": false,
  "required": [
    "id",
    "type",
    "api_version",
    "created_at",
    "environment",
    "vendor_id",
    "data"
  ],
  "properties": {
    "id": {
      "type": "string",
      "description": "Event ID. Equals the X-PFC-Webhook-Id header. A replay keeps it."
    },
    "type": {
      "type": "string",
      "enum": [
        "vendor.license.cancelled",
        "vendor.license.email_changed",
        "vendor.license.provisioning_blocked"
      ]
    },
    "api_version": {
      "type": "string",
      "const": "v1"
    },
    "created_at": {
      "type": "string",
      "format": "date-time",
      "description": "When the change happened."
    },
    "environment": {
      "type": "string",
      "enum": [
        "live"
      ]
    },
    "vendor_id": {
      "type": "string"
    },
    "data": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "source",
        "revision",
        "license"
      ],
      "properties": {
        "source": {
          "type": "string",
          "enum": [
            "vendor_admin",
            "provisioning"
          ],
          "description": "vendor_admin: an administrator made the change in Vendor Admin. provisioning: the license became blocked while it was being applied."
        },
        "revision": {
          "$ref": "#/components/schemas/Revision"
        },
        "error_code": {
          "type": "string",
          "description": "Only on vendor.license.provisioning_blocked: the same blocked code GET returns in provisioning.error_code.",
          "enum": [
            "SPONSORED_LICENSE_IDENTITY_CONFLICT",
            "SPONSORED_LICENSE_LEGACY_REVIEW_REQUIRED",
            "SPONSORED_ENTITLEMENT_CONFLICT",
            "SPONSORED_LICENSE_EMAIL_IN_USE"
          ]
        },
        "license": {
          "type": "object",
          "description": "The license when the event was sent. Apply it only if its revision is higher than the one you hold.",
          "additionalProperties": false,
          "required": [
            "id",
            "external_customer_id",
            "email",
            "active",
            "revision"
          ],
          "properties": {
            "id": {
              "type": "string",
              "pattern": "^slic_[a-f0-9]{64}$"
            },
            "external_customer_id": {
              "$ref": "#/components/schemas/ExternalCustomerId"
            },
            "email": {
              "type": "string",
              "format": "email",
              "maxLength": 254
            },
            "active": {
              "type": "boolean"
            },
            "plan": {
              "type": "string",
              "enum": [
                "monthly",
                "lifetime"
              ],
              "description": "Absent for sponsored access."
            },
            "revision": {
              "$ref": "#/components/schemas/Revision"
            }
          }
        }
      }
    }
  }
}
```
