Quick start
Three endpoints connect your checkout, customer account and download button.
/vendor/v1/sponsored-licenses/{external_customer_id} GETCheck license status/vendor/v1/sponsored-licenses/{external_customer_id} POSTRequest an addon download/vendor/v1/addon-downloads - Create a server API key. In Vendor Admin, choose License management and downloads. Store the secret on your server.
- Activate access after a confirmed purchase or eligibility change. Send a PUT with the customer's email, your stable customer ID, active state, revision and the plan required by your approved model. Save the job before sending it so retries are safe.
- Offer setup and download buttons. Once the response has
status: "active", send the customer toaccount_urlto sign in and let your server request a download URL when they click Download.
Base URL: https://propfirmconnector.com. All examples use placeholder customer data. The curl examples use a POSIX shell and a server environment variable; replace the example customer ID and email for an authorized purchase, and generate your own idempotency key.
The first example uses the monthly/lifetime model. If your firm has sponsored access, omit the plan field as explained below.
curl --request PUT 'https://propfirmconnector.com/vendor/v1/sponsored-licenses/customer_123' \
--header "Authorization: Bearer $PFC_VENDOR_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'Idempotency-Key: 76fc3221-7aab-445f-9027-986736130871' \
--data '{"email":"customer@example.com","plan":"monthly","active":true,"revision":1}' API key & permissions
Sign in to Vendor Admin with your approved administrator account and complete the authenticator prompts. Choose a key type, create the key, and copy its one-time secret to your server secret manager, for example as PFC_VENDOR_API_KEY. If the type you need is not listed, contact support.
Key types
| Key type | Permissions | What it can do | Choose it when |
|---|---|---|---|
| License management and downloads | sponsored_licenses:writesponsored_licenses:readaddon:download | Issue, change and check licenses. Request addon download links. | One server handles both. The usual License API key. |
| License management | sponsored_licenses:writesponsored_licenses:read | Issue, change and check licenses. | A different server requests the downloads. |
| Addon download only | addon:download | Request addon download links. | A delivery server that must not change licenses. |
| Discount codes | discount_codes:creatediscount_codes:readredemptions:list | Create discount codes, check one, and list redemptions. | You use the Coupon API. No other key type can create codes. |
| Audit log (read only) | audit_events:read | Read your audit log. | A monitoring or security tool. |
- Vendor Admin lists only the types your organization is approved for.
- A key keeps its type. Rotating it keeps the same permissions; to change type, create a new key and revoke the old one.
- Each environment allows two usable keys in total, across all types, and only one of them may be pending (created but not yet used). With two in use, revoke one before creating or rotating another.
Use Authorization: Bearer <your-secret> for every endpoint. The key selects your organization; there is no vendor or environment parameter.
A new key starts pending and must be used within 24 hours. Its first authenticated API request activates it. For a read-only credential check, GET a new, valid customer ID: a 404 SPONSORED_LICENSE_NOT_FOUND confirms authentication without issuing a license. A key with only download permission cannot use that check.
Keep the key out of browsers, public environment variables, URLs, source control and logs. Call only the fixed API origin, refuse redirects on authenticated API requests, and use a timeout. Do not put production credentials in development or staging.
For rotation, deploy the replacement secret and verify it before revoking the old key. Report a suspected leak promptly and revoke the affected key.
Choose your approved model
PropFirmConnector confirms your model during onboarding. Your key's configuration selects it; a request cannot switch models or grant itself additional permissions.
Monthly & lifetime
Every PUT requires email, plan, active and revision. Monthly access can be disabled; lifetime access is permanent. Responses include plan.
Sponsored access
Every PUT contains only email, active and revision. Omit plan entirely; sending it is rejected. Responses omit plan. Access shows your firm's configured name and can be enabled or disabled.
{"email":"customer@example.com","active":true,"revision":1} Both models use the same endpoints, permissions, identity mapping, idempotency and download flow. For sponsored access, set active: false with a higher revision when eligibility ends; reactivation uses the same ID and another higher revision.
Customer identity & setup
Use one durable external_customer_id per customer in your system, such as your customer UUID. It must match [A-Za-z0-9_-]{1,128} and is case-insensitive: Trader_01 and trader_01 are the same customer, and responses return the lowercase form. Reuse it across purchases, cancellations, reactivations, upgrades and email changes. Do not use an email address or an individual order ID.
Read the customer's verified email from your trusted account records. PropFirmConnector creates an account if the email is new, or adds a separate license slot to an existing account. You decide which email holds a monthly or sponsored license: see Change a customer email. A lifetime license is locked to the email it was issued to.
Each customer mapping grants one license slot from your firm. Additional purchases reuse that slot. One email holds one license from your firm at a time: a new customer ID for an email whose license is active is refused with 409 SPONSORED_LICENSE_EMAIL_IN_USE, which names your existing customer ID, and nothing is recorded. If that license is cancelled, the new customer ID takes the account over. It is a new license, billed from its activation; reuse the original customer ID to keep paid coverage. Licenses the customer bought separately remain independent.
What your customer does
- Open the returned
account_urland sign in using the same email supplied by your firm. - Complete email verification and any account security prompts shown by PropFirmConnector.
- Install the addon in NinjaTrader and follow its Activate this device browser flow. The customer signs in to authorize the device; no license-key entry is required.
The API does not send an onboarding email or return a password or license key. Provide the account link in your confirmation page or your own customer email. Customers can use Google sign-in or an email sign-in link with the supplied email. See the connection guides for setup instructions.
Issue or update a license
/vendor/v1/sponsored-licenses/{external_customer_id}Requires sponsored_licenses:write, Content-Type: application/json, and an Idempotency-Key. For the monthly/lifetime model, the body contains these four fields, plus the optional previous_email guard. Sponsored-access integrations omit plan.
- email · string
- The email that should hold the license, trimmed and lowercased, at most 254 characters. A newer revision with a different email moves the license.
- previous_email · string
- Optional guard for an email change: the address you believe is on record. If it is not, nothing moves.
- plan · string
monthlyorlifetime. Required on every request in the monthly/lifetime model; forbidden in the sponsored-access model.- active · boolean
trueto activate.falsecan disable monthly or sponsored access, never lifetime.- revision · integer
- Start at 1; increase for every committed change to this customer's desired state. Maximum 9,007,199,254,740,991. Never reset after cancellation.
Prices, quantities, trial periods, account IDs and other extra fields are rejected. The maximum body is 4,096 UTF-8 bytes.
{
"id": "slic_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"external_customer_id": "customer_123",
"plan": "monthly",
"active": true,
"revision": 1,
"status": "active",
"email_changed": false,
"provisioning": {
"state": "ready",
"error_code": null,
"retryable": false
},
"account_url": "https://propfirmconnector.com/account",
"replayed": false,
"stale": false
} - id
- Opaque API resource identifier. This is not the customer's license key.
- plan / active / revision
- The currently accepted desired state. An older request may return a newer state.
- status
active,inactive, orpending. Pending means the state has been accepted but is not fully applied. Do not treat it as ready for use.- account_url
- Customer sign-in and license setup page.
- email_changed
truewhile the current revision is the one that moved the license to a different email. Check it to notice a move you did not intend.- replayed / stale
- Whether this PUT repeated an accepted operation, or was older than the current revision. Reconcile against the returned state.
All three endpoints return Cache-Control: no-store. No response exposes other licenses, customer payment history or the customer's license key.
Monthly & lifetime rules
Monthly: your firm manages active access
The customer's account shows your firm’s configured name. Access stays enabled until your firm disables it; it does not expire automatically after 30 days. When the customer no longer qualifies, send the same ID and email with a higher revision:
{"email":"customer@example.com","plan":"monthly","active":false,"revision":2} Reactivate with active: true and the next revision. A deactivation received before any activation records the revision without creating an account or charging for a license. Disabling this slot preserves separately purchased licenses; device capacity is pooled, so it does not target a named physical computer.
Changes made in Vendor Admin
An administrator can cancel a monthly or sponsored license, or change its email, from the Usage page in Vendor Admin. Each is the same change as the PUT and uses the next revision. With a webhook enabled, each also sends your server an event. Your system remains the source of truth: make the same change there, because your next PUT states email and active again and the license follows it. If your next PUT reuses that revision it receives 409 SPONSORED_LICENSE_REVISION_CONFLICT; read the current state with GET and send a higher revision. Lifetime licenses cannot be cancelled or re-emailed there either.
Lifetime: permanent access
The customer's account shows an ordinary Lifetime license, without your firm branding. Issue a new lifetime license with plan: "lifetime", active: true and revision 1.
To upgrade an existing monthly license, use its same customer ID and email with a higher revision. This converts the existing slot to lifetime; it does not add another slot from your firm.
{"email":"customer@example.com","plan":"lifetime","active":true,"revision":3} Every new change needs a new idempotency key. A lifetime downgrade or disable attempt returns 409 SPONSORED_LICENSE_LIFETIME_PERMANENT, including delayed monthly updates.
Change a customer email
Send the new email with the next revision. Your firm decides which email holds the license; the customer is not asked to confirm.
{"email":"new-address@example.com","plan":"monthly","active":true,"revision":3} - The license moves. Same customer ID, revision history and billing. The account for the new email is created if it does not exist. Nothing is charged.
- The old account loses only this license. Licenses the customer bought separately stay. The old email can be provisioned again under another customer ID.
- The customer signs in with the new email at
account_urland activates their device again. - The same request may change
active. Cancel and re-email, or reactivate a returning customer under a new email, in one revision. A plan change needs its own revision. - Lifetime licenses cannot change email. The request returns
409 SPONSORED_LICENSE_LIFETIME_PERMANENT.
The response has email_changed: true on the revision that moved the license. An older revision never moves anything; it returns the current state with stale: true. If the response is pending, retry the identical request; the old account keeps access until the new one holds the license.
Optional guard
Add previous_email to move the license only if the email on record is the one you expect. Otherwise the request returns 409 SPONSORED_LICENSE_PREVIOUS_EMAIL_MISMATCH and nothing moves.
{"email":"new-address@example.com","previous_email":"customer@example.com","plan":"monthly","active":true,"revision":3} Check license status
/vendor/v1/sponsored-licenses/{external_customer_id}Requires sponsored_licenses:read. Send no body or query parameters. A successful response contains the same core fields as PUT, without replayed or stale. An unknown customer mapping returns 404.
curl 'https://propfirmconnector.com/vendor/v1/sponsored-licenses/customer_123' \
--header "Authorization: Bearer $PFC_VENDOR_API_KEY" \
--header 'Accept: application/json' Use GET to reconcile uncertain deliveries and recover after outages. GET does not finish a pending activation. Check provisioning.state: ready means the revision is applied; pending can be retried; blocked needs operator intervention. The existing status remains pending for an unapplied revision, including a blocked request.
When provisioning.retryable is true, retry the original PUT with the same body, revision and idempotency key using bounded backoff. When blocked, stop automatic retries, retain the job, and resolve provisioning.error_code. Keep the original customer ID; creating a different ID cannot resolve an account-binding conflict.
{
"id": "slic_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"external_customer_id": "customer_123",
"plan": "monthly",
"active": true,
"revision": 1,
"status": "pending",
"email_changed": false,
"provisioning": {
"state": "blocked",
"error_code": "SPONSORED_LICENSE_IDENTITY_CONFLICT",
"retryable": false
},
"account_url": "https://propfirmconnector.com/account"
} Reliable updates & retries
- In one database transaction, save the customer's desired state, next revision, and an outgoing job. Generate a random UUID for that job's idempotency key.
- Send the saved job from your server. Base changes on current committed customer state, not payment webhook arrival order. Duplicate payment events should not create duplicate jobs.
- On a timeout, uncertain response, HTTP 408, 429 or temporary 5xx, retry the exact body, revision and key with bounded exponential backoff and jitter. Honor
Retry-Afterwhen present. - If HTTP 200 has
status: "pending", inspectprovisioning. Retry only whenretryableis true; ablockedresult requires intervention. Mark the job complete only after confirming the returned state is applied, or reconciling a newer applied state. - Retain exhausted jobs and alert your operator. Stop automatic retries on other 4xx errors, except the recoverable download-pending case described below.
Idempotency-Key accepts 16–128 ASCII letters, digits, dots, underscores, colons and hyphens, starting with a letter or digit. Do not include customer information. Records are kept for 90 days; customer bindings and revision ordering remain durable.
The same revision with different state, or the same idempotency key for a different operation, returns 409. Older updates normally return the current state with stale: true. Lifetime disable and downgrade requests are always rejected. Never change the ID or generate a new key just to bypass a conflict.
Webhooks
Optional. Your own PUT already returns its result, so events cover only what your server did not do: a change an administrator made in Vendor Admin, and a license that became blocked.
| Event | Sent when |
|---|---|
vendor.license.cancelled | An administrator cancels a license on the Usage page. |
vendor.license.email_changed | An administrator moves a license to another email. data.license.email is the new one. |
vendor.license.provisioning_blocked | Provisioning stops with a blocked error_code that a retry will not clear. |
{
"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
}
}
} data.revision is the revision the change used. data.license is the license when the event was sent: apply it only if its revision is higher than the one you hold, then continue from that revision. Deduplicate by event id.
Endpoint rules, signature verification, retries and replay are the same as the Coupon API: see the webhook reference. Set the endpoint and follow every delivery on the Webhooks page of Vendor Admin.
Download the addon
/vendor/v1/addon-downloadsRequires addon:download on the same key and Content-Type: application/json. The only body field is external_customer_id. No idempotency key is required.
curl --request POST 'https://propfirmconnector.com/vendor/v1/addon-downloads' \
--header "Authorization: Bearer $PFC_VENDOR_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{"external_customer_id":"customer_123"}' {
"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"
} The response describes the latest promoted production release: version and release_id identify it, sha256 is the ZIP checksum, size_bytes is its size, and expires_at is the URL expiry. Use the returned URL as-is; do not construct storage paths or pin these example values.
- The customer clicks a Download button in their signed-in account on your website.
- The button POSTs to your firm's server, protected by your normal session and CSRF controls.
- Your server derives the customer ID and eligibility from your database and calls this endpoint. Never trust a customer ID or email supplied by the browser.
- Your server returns a 303 redirect to
url, withCache-Control: no-storeandReferrer-Policy: no-referrer. Storage serves the ZIP directly.
The customer must have active, fully applied access issued by your firm: monthly, lifetime or sponsored, according to your configuration. A separate purchased license alone does not qualify for this vendor download endpoint. Downloads do not create or repair licenses.
A signed URL lasts five minutes and can be reused until it expires. Disabling a monthly license blocks new links; already issued links can work until expiry. Request links only on a deliberate click, not during page rendering or prefetch. Do not persist, email, log or send signed URLs to analytics. The addon still requires license activation after installation.
Server download example
This Node.js example uses built-in fetch. Connect the named authentication, CSRF and database helpers to your application and expose it as a POST route. Use a normal form submission or browser navigation for the redirect; a background fetch alone does not initiate a browser file download.
// Node.js server example. Wire these helpers into your own application.
// requireCustomer and assertCsrf must reject unauthenticated/invalid requests.
// loadLicenseForCustomer reads YOUR database, never a caller-supplied customer ID.
async function downloadAddon(request) {
const customer = await requireCustomer(request);
await assertCsrf(request);
const license = await loadLicenseForCustomer(customer.id);
if (!license || license.status !== 'active') {
return new Response('Your license is not ready.', { status: 403 });
}
const key = process.env.PFC_VENDOR_API_KEY;
if (!key) throw new Error('PFC API key is not configured');
try {
const result = await fetch(
'https://propfirmconnector.com/vendor/v1/addon-downloads',
{
method: 'POST',
redirect: 'error',
signal: AbortSignal.timeout(15000),
headers: {
Authorization: 'Bearer ' + key,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
external_customer_id: license.externalCustomerId,
}),
},
);
if (!result.ok) {
const failure = await result.json().catch(() => ({}));
const headers = { 'Cache-Control': 'no-store' };
const retry = Number(result.headers.get('Retry-After'));
if (Number.isFinite(retry) && retry > 0) {
headers['Retry-After'] = String(Math.min(3600, Math.ceil(retry)));
}
if (result.status === 403) {
return new Response('This account is not eligible for a download.', {
status: 403, headers,
});
}
if (result.status === 409 && failure.code === 'ADDON_DOWNLOAD_PENDING') {
// Resume the customer's original provisioning job in your queue.
// Do not issue a new license or change its idempotency key.
return new Response('License setup is still processing.', {
status: 409, headers,
});
}
if (result.status === 429) {
return new Response('Please wait before requesting another download.', {
status: 429, headers,
});
}
if (result.status >= 400 && result.status < 500 && result.status !== 408) {
// Alert your operator using only the status and public error code.
// This configuration or request error needs a fix, not blind retries.
return new Response('Download unavailable. Please contact support.', {
status: 502, headers,
});
}
return new Response('Download unavailable. Please try again later.', {
status: 503, headers,
});
}
const download = await result.json();
const url = new URL(download.url);
if (url.protocol !== 'https:' || url.hostname !== 'storage.googleapis.com'
|| url.username || url.password || url.port) {
throw new Error('Unexpected download destination');
}
return new Response(null, {
status: 303,
headers: {
Location: url.href,
'Cache-Control': 'no-store',
'Referrer-Policy': 'no-referrer',
},
});
} catch {
return new Response('Download unavailable. Please try again later.', {
status: 503, headers: { 'Cache-Control': 'no-store' },
});
}
} Errors
Error bodies contain code and error. Branch on the HTTP status and code; human-readable wording may change. Accept additional response fields.
{
"error": "Lifetime licenses cannot be disabled.",
"code": "SPONSORED_LICENSE_LIFETIME_PERMANENT"
} | HTTP | Code | What to do |
|---|---|---|
| 400 / 415 | SPONSORED_LICENSE_INVALID_REQUEST / ADDON_DOWNLOAD_INVALID_REQUEST | Fix the path, JSON, required fields, idempotency header, or Content-Type. Do not retry unchanged. |
| 401 | VENDOR_AUTHENTICATION_FAILED | Check the server secret, key status and permissions. An expired, revoked, malformed or insufficiently scoped key cannot authorize the request. |
| 403 | SPONSORED_LICENSE_FORBIDDEN / ADDON_DOWNLOAD_FORBIDDEN | Confirm your firm is approved for the requested operation. For downloads, also confirm this customer has active access issued by your firm. Contact support if these conditions are met. |
| 404 | SPONSORED_LICENSE_NOT_FOUND | No license mapping exists for this customer under your key. Check the durable customer ID. A PUT returns this only when previous_email is sent for a customer that was never provisioned. |
| 409 | SPONSORED_LICENSE_IDENTITY_CONFLICT | The account records for that email conflict and need PropFirmConnector support review. Stop automatic retries. |
| 409 | SPONSORED_LICENSE_PREVIOUS_EMAIL_MISMATCH | You sent the optional previous_email guard and it is not the email on record. Nothing moved. Resend with the right value, or without the guard. |
| 409 | SPONSORED_LICENSE_EMAIL_IN_USE | That email has an active license from your firm under another customer ID, returned in conflicting_external_customer_id. Nothing was recorded. Keep using that customer ID, or cancel its license first. |
| 409 | SPONSORED_LICENSE_REVISION_CONFLICT | The same revision was used for different state. Reconcile your committed customer state before producing the next revision. |
| 409 | SPONSORED_LICENSE_IDEMPOTENCY_CONFLICT | An idempotency key was reused for a different operation. Recover the original job and response before sending a new change. |
| 409 | SPONSORED_LICENSE_LIFETIME_PERMANENT | Lifetime access cannot be disabled, downgraded or moved to another email. Stop this job and correct your logic. |
| 409 | SPONSORED_LICENSE_LEGACY_REVIEW_REQUIRED | An existing account needs review by PropFirmConnector support before the change can finish. |
| 409 | SPONSORED_ENTITLEMENT_CONFLICT | Conflicting customer grants need PropFirmConnector support review. Stop automatic retries; do not create another customer ID. |
| 409 | ADDON_DOWNLOAD_PENDING | License setup is still being applied. Retry the original provisioning PUT, then request a download after status becomes active. |
| 413 | VENDOR_REQUEST_TOO_LARGE | Reduce the body to the documented fields and byte limit. |
| 429 | VENDOR_RATE_LIMITED / SPONSORED_LICENSE_DAILY_LIMIT / ADDON_DOWNLOAD_RATE_LIMITED | Honor Retry-After and queue the work. Contact support before a launch that needs higher limits. |
| 503 | SPONSORED_LICENSE_PROJECTION_PENDING / SPONSORED_LICENSE_BILLING_PENDING | Temporary processing delay. Retry the exact PUT with backoff; honor Retry-After when present. |
| 503 | SPONSORED_LICENSE_UNAVAILABLE / ADDON_DOWNLOAD_UNAVAILABLE / VENDOR_PROMOTION_SERVICE_UNAVAILABLE | Temporarily unavailable or not configured for this environment. Retry with backoff and contact support if it persists. |
| 5xx | SERVICE_UNAVAILABLE | Temporary server failure. Retain the job and retry safely. |
| 405 | METHOD_NOT_ALLOWED | Use the documented HTTP method. The response also includes allowed. |
Every response includes a server-generated X-Correlation-ID. Paste it into the search box of the Audit log in your vendor portal to find that request, its result and the API key that made it. For support, include that ID, the endpoint, approximate UTC time, HTTP status, error code, customer ID and revision. Share the idempotency key when investigating a write. Never send your API secret or signed download URL.
Audit log
The Audit log page of Vendor Admin lists every authenticated API request and every portal change for your organization, newest first, for 90 days. Each request shows the operation, HTTP status, error code, API key and source IP address. It never stores a customer email or a request body.
Every API response carries X-Correlation-ID. Paste it into the Audit log search to open that request.
Enter an exact customer ID to list that customer's requests.
Filter by event type, result, API key and date, and export the result as CSV or JSON (up to 10,000 events per export).
Rejected API keys
A request that names one of your key IDs but fails authentication still receives the generic 401. Your log lists it
under Rejected API keys with its source IP address and the reason. At most 20 are recorded per key and 60 per
organization each hour. A request with an unknown key ID is not recorded.
| failure_reason | Meaning | What to do |
|---|---|---|
secret_mismatch | The key ID is yours, but the secret is wrong. | Deploy the correct secret. If the request is not yours, the key ID leaked but the secret did not. |
key_revoked | A revoked key is still in use. | Find the server at the listed source IP and give it the current key. |
key_expired | The key expired, or was never used within 24 hours of creation. | Create a new key. |
key_inactive | The key is not one of your usable keys. | Create a new key. |
scope_denied | The key does not have the permission this endpoint needs. | Use a key created with the right purpose. |
key_invalid | The key could not be verified. | Create a new key; contact support if it repeats. |
Email alerts
Every active administrator of the environment is emailed when:
- an API key is created, replaced or revoked;
- a webhook endpoint is saved, webhook delivery is turned off, or the signing secret is replaced;
- 5 or more requests are rejected for their key within 15 minutes;
- 20 or more other requests fail within 15 minutes and they are at least half of your traffic;
- 3 webhook deliveries in a row fail, or an event uses all 12 attempts.
The last three repeat at most once every 6 hours each, and each administrator can turn them off for themselves on the Notifications page of Vendor Admin. The first two cannot be turned off. Alerts contain no customer data.
Read the log from your server
In Vendor Admin, create a key with the purpose Audit log (read only). Only that key has
audit_events:read, and it can do nothing else: the log names your administrators, so your other keys cannot read it.
It uses one of your two key slots for that environment. Then call GET /vendor/v1/audit-events.
| Query parameter | Value |
|---|---|
limit | 1–100, default 50. |
cursor | page.next_cursor from the previous page. Send the same filters with it. |
category | api, portal, or one group: api.sponsored_license_sync, api.sponsored_license_status, api.addon_download, api.authentication_failed, portal.api_keys, portal.webhooks, portal.licenses, portal.access. |
outcome | success, failed (rejected or failed), or error (server errors and unfinished requests). |
from, to | UTC instants such as 2026-09-01T00:00:00Z. |
key_id | Only requests made with this API key ID. |
external_customer_id | Only events for this exact customer ID. Combine only with from and to. |
event_id | One event by its ID or X-Correlation-ID. Send it alone. |
All parameters are optional. Events are newest first. While page.has_more is true, send page.next_cursor
unchanged with the same filters. id is the request's X-Correlation-ID. outcome is
success, failed, error or incomplete. actor.type is
api_key, admin (with email and role) or support.
http_status, error_code, failure_reason and source_ip are null when they do not
apply. Accept additional fields. Each read is itself recorded in the log. Limit: 60 requests per minute.
curl 'https://propfirmconnector.com/vendor/v1/audit-events?category=api.authentication_failed&limit=50' \
--header "Authorization: Bearer $PFC_AUDIT_LOG_KEY" \
--header 'Accept: application/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 | Code | What to do |
|---|---|---|
| 400 | AUDIT_EVENTS_INVALID_REQUEST | Unknown, repeated or malformed parameter, or a cursor sent with different filters. Do not retry unchanged. |
| 401 | VENDOR_AUTHENTICATION_FAILED | The key is wrong, retired, or was not created with the Audit log purpose. |
| 404 | NOT_FOUND | No event has that event_id in your organization. |
| 429 | VENDOR_RATE_LIMITED | More than 60 requests in a minute. Honor Retry-After. |
| 503 | AUDIT_EVENTS_UNAVAILABLE | Retry with backoff. |
Limits & transport
| Operation | Limit |
|---|---|
| License PUT | 30 requests per minute per vendor. |
| License GET | 120 requests per minute per vendor. |
| First activation | 100 reservations per vendor per UTC calendar day. Failed account creation still uses its reservation; retries reuse it. An email change uses one only when the new email needs a new account; email changes themselves are not limited. |
| Download POST | 120 requests per minute per vendor, plus 30 per hour per customer. |
| Audit events GET | 60 requests per minute per vendor. |
| Request body | License PUT: 4,096 bytes. Download POST: 1,024 bytes. GET: no body. |
Limits are shared across your keys. Use HTTPS and JSON. Only the audit events endpoint accepts query parameters, and only the ones listed for it. No endpoint accepts extra path segments, a trailing slash after the resource, or unknown JSON fields. These are server-to-server endpoints; a browser CORS integration is not supported.
Honor the Retry-After seconds on 429 responses. The daily cap waits until the next UTC day; per-customer download limits may require up to an hour. Other temporary errors may omit this header, so use your own bounded backoff.
Billing behavior
For the monthly/lifetime model, your firm charges customers on its own website, at whatever price you choose. PropFirmConnector invoices your firm monthly for license charges accrued during the billing period, at your agreed rates. API request count is not the invoice basis; status reads, downloads and safe retries have no separate license charge. Sponsored-access terms are agreed separately.
- Monthly: the first successful activation starts a paid 30-day period. Another period is charged every 30 days while access remains enabled. You do not need to call the API to trigger each renewal.
- Deactivation: stops future monthly renewals. There is no automatic prorated credit or refund for the current period.
- Reactivation: within the paid period adds no charge and retains the same renewal date. After that period has ended, reactivation starts a new charged 30-day period. Disabled time is not billed.
- Lifetime: one charge on successful activation. Repeated enables do not create another charge.
- Monthly to lifetime: one full lifetime charge, with future monthly renewals stopped. Previously billed monthly periods are not automatically credited.
Keep monthly eligibility synchronized promptly to avoid further renewals after a customer stops qualifying. Your monthly invoice can include renewals for customers first provisioned in an earlier month.
Launch & support
Accept additive response fields. Breaking changes require a new API version.
There is no public sandbox for this license API. Develop against local mocked responses. Coordinate the first live activation with PropFirmConnector; production requests are real grants and lifetime activation is permanent.
Before launch, confirm your integration handles duplicate checkout events, lost responses, pending activation, out-of-order updates, monthly cancellation, lifetime cancellation protection, key rotation and customer download authorization. Persist your customer mapping and revision counter so a restart cannot reset them.
Request access or create a key
Request access with your firm’s website and preferred model. Already approved? Open Vendor Admin to create your key, or contact support to coordinate your first activation.
Integration help: support@propfirmconnector.com