Developer reference · API v1

Coupon API reference

Issue and track single-use PropFirmConnector discount codes from your backend. Customers redeem their code at PropFirmConnector checkout; your server tracks the resulting license through status reads and optional webhooks.

API version v1 Last updated 2026-09-08 HTTPS + JSON
Explore this guide
01 / Get started

Overview#

Your server creates a code for an authenticated customer. The customer completes checkout on PropFirmConnector, which handles payment and license activation. Save the returned code ID against your customer so you can track redemption and the resulting license.

  1. Set up your approved offer. Complete onboarding and store your production API secret on your server.
  2. Give the customer one code. Create it from your backend with an idempotency key. The customer redeems it at PropFirmConnector checkout.
  3. Keep your records up to date. Read code status and reconcile redemptions. Enable signed webhooks if you want notifications.

Coupon model rules

  • One code purchases one independent PropFirmConnector license slot.
  • The same PropFirmConnector account may redeem more than one code. Codes do not stack on one slot and cannot be combined with another discount.
  • Each code can be redeemed once for a plan included in your approved offer.
  • A customer with no PropFirmConnector trial, license, entitlement, or billing history receives the normal seven-day free trial. Activating that trial consumes the code even if the customer later cancels or never makes a paid payment.
  • A customer with any PropFirmConnector history is charged immediately. This includes an ended or canceled trial, an inactive or reversed entitlement, and a current or prior paid entitlement. The vendor cannot select or override this.
  • Canceling Monthly and later starting a new billing agreement does not preserve a prior vendor offer. A new, unused code is required.

Onboarding sets your allowed plans, code expiry, and either one percentage discount or a separate fixed discount amount for each allowed plan. API requests cannot change these terms. The API and its webhooks do not return payment amounts or provider-specific payment details, or report commissions, fees, payouts, or revenue shares.

Environments & transport#

All requests use HTTPS with TLS 1.2 or newer and JSON encoded as UTF-8. The stable resource paths are:

Resource paths
POST /vendor/v1/discount-codes
GET  /vendor/v1/discount-codes/{discount_code_id}
GET  /vendor/v1/redemptions

The production base URL is https://propfirmconnector.com. Your key determines the vendor and environment for every request. You cannot read another vendor's records or use its pagination cursors.

Requests with a body must send Content-Type: application/json.

The shell examples below read your key from the server environment variable PFC_VENDOR_API_KEY.

Authentication#

Each approved vendor receives an opaque bearer API key bound to one environment; production keys begin with pfc_live_. A key has this server-generated shape:

Key format
pfc_{test|live}_{key_id}_{43-character-base64url-secret}

Use the complete key as the bearer value. It identifies your vendor, environment, and permissions. Vendor Admin displays it once; save it immediately because it cannot be retrieved later.

Keep the key server-side. The bearer key must live in your server-side secret manager. It must never be embedded in a browser, mobile app, URL, source repository, analytics event, customer-facing page, or client-side storage. Your backend calls this API after authenticating your own customer.

Required headers#

Every request
Authorization: Bearer pfc_live_k0123456789abcdef01234567_<43-character-secret>

Create requests also include Idempotency-Key; authenticated reads do not. Supplying multiple or malformed Authorization values is rejected. Launch keys normally receive these scopes:

  • discount_codes:create for POST /vendor/v1/discount-codes
  • discount_codes:read for GET /vendor/v1/discount-codes/{discount_code_id}
  • redemptions:list for GET /vendor/v1/redemptions

Missing, malformed, wrong-environment, out-of-scope, expired, and revoked keys return 401 VENDOR_AUTHENTICATION_FAILED. Check the key's environment, permissions, and status in Vendor Admin before retrying.

Key lifecycle and rotation#

A newly generated key begins as pending and expires if it is not used within 24 hours. Its first valid, in-scope request atomically activates it. Each vendor and environment permits at most two non-expired pending-or-active keys, and at most one pending key, which supports a clean rotation overlap:

  1. Create a replacement key and store its one-time bearer value immediately.
  2. Send one valid, in-scope request with it; the key becomes active.
  3. Confirm successful traffic, then revoke the old key.

If the one-time display is lost, revoke that pending key and create another — PropFirmConnector cannot recover it. A compromised key must be revoked immediately. Revocation is authoritative on the next request and never alters already-created discount codes or entitlements.

API keys and webhook signing secrets rotate independently: the webhook_secret is used only by your receiver to verify callbacks PropFirmConnector sends you, and it never authenticates inbound API requests. Key and webhook management is performed by your enrolled administrators in the vendor portal, which requires authenticator-app (TOTP) sign-in.

Idempotency#

POST /vendor/v1/discount-codes requires an idempotency key:

Header
Idempotency-Key: 92e671d3-7283-4cb8-93fb-1f3fcd8db36c

The value must be 16–128 characters, start with an ASCII letter or digit, and contain only ASCII letters, digits, ., _, :, or -. Use a random UUID generated for one create operation, and never encode customer information into it. Its server-side scope is the authenticated vendor and environment. Idempotency records are retained for 12 months:

  • Repeating the same create with the same key and byte-identical body returns the same code and promotion ID. A new operation returns 201; a replay returns 200 with replayed: true.
  • Reusing the key with a different body returns 409 with error code VENDOR_PROMOTION_CONFLICT and creates no code.
  • Concurrent duplicates serialize to one creation and an idempotent replay.
  • After 12 months the record may be deleted. Never intentionally reuse an old key — a reused key whose record aged out would mint a second code and a second license slot.
Recover lost responses by retrying, never by re-keying. If the network drops after a create request, retry the same body with the same Idempotency-Key. Switching to a new key merely because the first response was lost can create a second valid code — and therefore a second license slot.

Identity boundary#

The create request contains no customer identity and no vendor reference. You never send a customer ID, name, email address, or account identifier to PropFirmConnector. Instead, you privately associate the returned discount_code_id and raw code with your customer in your database. Use that ID for all later status reads and reconciliation.

Anyone holding an unused code may redeem it. Show it only inside the intended customer's authenticated session, and keep it out of analytics, URLs, support transcripts, and shared logs. The first eligible PropFirmConnector account to complete redemption consumes it.

At redemption, PropFirmConnector records and returns two identity snapshots:

pfc_account_name
The PropFirmConnector profile name at redemption. It is not a verified legal name.
pfc_account_email
The normalized, verified PropFirmConnector email at redemption.

Both values are immutable snapshots — they do not change if the profile changes later. No internal account UID, payment identifier, or other account identifier is ever exposed. Reconcile the snapshots to your own customer through your private discount_code_id mapping, and do not treat either snapshot as standalone proof of identity.

02 / Codes & endpoints

Discount codes#

The customer-facing code contains eight characters displayed as XXXX-XXXX, drawn from the Crockford Base32 alphabet:

Alphabet and canonical form
0123456789ABCDEFGHJKMNPQRSTVWXYZ

^[0-9A-HJ-NP-TV-Z]{4}-[0-9A-HJ-NP-TV-Z]{4}$

The alphabet omits I, L, O, and U. Codes are case-insensitive at checkout; O is accepted as an alias for 0, and I or L as aliases for 1. The hyphen may be omitted on input. Display the code exactly as returned by the API.

Lifecycle#

A code expires at its offer-configured deadline; the launch offers use exactly 24 hours after created_at. Starting an eligible checkout before expires_at may reserve the code while payment is confirmed. That checkout can complete after expiry; expiry blocks new checkout attempts without canceling a payment already in progress.

If a code remains reserved, wait for a status update or contact support before issuing a replacement. No customer name or email is returned until the code is consumed.

An unredeemed code has external status: unused. Availability is expressed separately:

availabilityMeaning
availableThe code can start checkout. redeemable is true only in this state.
reservedOne authenticated checkout holds it while PropFirmConnector confirms or reconciles the payment operation.
expiredIts offer-configured first-use window ended.
revokedPropFirmConnector support disabled it before redemption.
consumedIt has been assigned to a license slot.

Once a trial is activated or an immediate purchase succeeds, the code is permanently consumed. Cancellation, payment failure, expiration of access, refund, reversal, or chargeback never makes it reusable.

The discount_code object#

Create, status, list, and webhook payloads all use the same canonical discount_code object. Fields that are not yet applicable are null — they are not omitted.

discount_code
{
  "discount_code_id": "dcode_01K2F6P8JKR6J0CG4M2Y7W0A3P",
  "code_masked": "****-7K9P",
  "status": "monthly_active",
  "status_version": 3,
  "status_changed_at": "2026-08-19T16:04:12.000Z",
  "availability": "consumed",
  "redeemable": false,
  "created_at": "2026-08-12T15:58:10.000Z",
  "expires_at": "2026-08-13T15:58:10.000Z",
  "redemption": {
    "redeemed_at": "2026-08-12T16:03:44.000Z",
    "pfc_account_name": "Alex Trader",
    "pfc_account_email": "trader+pfc@example.com"
  },
  "entitlement": {
    "plan": "monthly",
    "state": "monthly_active"
  },
  "pii_purged": false
}

Timestamps are RFC 3339 UTC strings with a Z suffix. status_version starts at 1 and increases monotonically whenever the external status changes for this discount_code_id. For a consumed code, entitlement.plan is monthly or lifetime, and entitlement.state derives from that code's license slot. Other licenses on the same account do not affect this status.

Status values#

statusMeaning
unusedNo trial or entitlement has consumed the code. Check availability and redeemable to distinguish available, reserved, expired, or revoked.
trial_activeThe code is consumed, the temporary seven-day entitlement is active, and no plan payment has completed. This is not a paid sale.
trial_ended_unpaidThe trial consumed the code but ended without the first plan payment. The entitlement is inactive.
monthly_activeThe code's Monthly entitlement currently qualifies for access, including cancel-at-period-end while already-paid access remains.
monthly_inactiveThe Monthly entitlement does not currently qualify for access. It may return to monthly_active if payment recovery succeeds within the same agreement.
lifetimeThe Lifetime payment completed and the permanent entitlement currently qualifies for access.
payment_reversedA payment supporting the entitlement was fully refunded, reversed, or charged back and the entitlement was deactivated. The code remains consumed.

State transitions#

Normal transitions
unused -> trial_active -> monthly_active
unused -> trial_active -> lifetime
unused -> trial_active -> trial_ended_unpaid
unused -> monthly_active
unused -> lifetime
monthly_active -> monthly_inactive
monthly_inactive -> monthly_active
monthly_active -> payment_reversed
lifetime -> payment_reversed

The direct unused → monthly_active|lifetime transitions are immediate checkouts for accounts with prior PropFirmConnector history. payment_reversed is terminal for that license slot in v1 — resolving a dispute does not silently restore it, and renewed access requires a new qualifying purchase with a new unused code. A trial remains trial_active until its first successful plan payment or until access ends; selecting Lifetime during a trial keeps trial_active until the exact Lifetime charge succeeds. Only a full refund, reversal, or chargeback produces payment_reversed — a partial refund does not change the status.

Create a discount code#

POST /vendor/v1/discount-codes

Send an empty JSON object with Authorization and Idempotency-Key. All body fields are rejected, including customer identity and offer settings. The maximum body size is 2,048 UTF-8 bytes.

Create request · shell
curl --request POST 'https://propfirmconnector.com/vendor/v1/discount-codes' \
  --header "Authorization: Bearer $PFC_VENDOR_API_KEY" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: 92e671d3-7283-4cb8-93fb-1f3fcd8db36c' \
  --data '{}'

Generate and save a new UUID for each intended code. Reuse that UUID only when retrying the same creation.

201 Created
HTTP/1.1 201 Created
Location: /vendor/v1/discount-codes/dcode_01K2F6P8JKR6J0CG4M2Y7W0A3P

{
  "replayed": false,
  "discount_code": {
    "discount_code_id": "dcode_01K2F6P8JKR6J0CG4M2Y7W0A3P",
    "code": "Q7KM-7K9P",
    "code_masked": "****-7K9P",
    "status": "unused",
    "status_version": 1,
    "status_changed_at": "2026-08-12T15:58:10.000Z",
    "availability": "available",
    "redeemable": true,
    "created_at": "2026-08-12T15:58:10.000Z",
    "expires_at": "2026-08-13T15:58:10.000Z",
    "redemption": null,
    "entitlement": null,
    "pii_purged": false
  }
}
Create responses include the raw code, including successful idempotent replays. Status reads, redemption lists, and webhooks contain code_masked only. Store the raw value securely if you need to show it again.

You may create more than one code for one customer — PropFirmConnector receives no issuance identity and performs no customer-level deduplication. Each code is a distinct potential license slot. Accidental duplicates are prevented by correct use of Idempotency-Key and your private discount_code_id mapping.

Get code & entitlement status#

GET /vendor/v1/discount-codes/{discount_code_id}

discount_code_id is the opaque ID returned at creation — never put the raw customer-facing code in this URL; the external status endpoint does not accept it. A successful lookup returns 200 with Cache-Control: no-store. The JSON response contains the current discount_code object under the discount_code property. Send no request body or query parameters.

Status request · shell
curl 'https://propfirmconnector.com/vendor/v1/discount-codes/dcode_01K2F6P8JKR6J0CG4M2Y7W0A3P' \
  --header "Authorization: Bearer $PFC_VENDOR_API_KEY"

A valid ID belonging to another vendor returns 404, as does an unknown ID. Use this endpoint for polling, confirmation, or recovery after missed webhooks. Conditional GET and ETag behavior are not part of v1.

List redeemed codes#

GET /vendor/v1/redemptions

Returns only codes that have been consumed — never status: unused, including expired unused codes. Results are ordered by redemption.redeemed_at descending.

ParameterRequiredRules
limitNoInteger 1–100. Default 50.
cursorNoOpaque cursor returned by the previous response.
First page · shell
curl --get 'https://propfirmconnector.com/vendor/v1/redemptions' \
  --header "Authorization: Bearer $PFC_VENDOR_API_KEY" \
  --data-urlencode 'limit=50'
200 OK
{
  "data": [
    {
      "discount_code_id": "dcode_01K2F6P8JKR6J0CG4M2Y7W0A3P",
      "code_masked": "****-7K9P",
      "status": "monthly_active",
      "status_version": 3,
      "status_changed_at": "2026-08-19T16:04:12.000Z",
      "availability": "consumed",
      "redeemable": false,
      "created_at": "2026-08-12T15:58:10.000Z",
      "expires_at": "2026-08-13T15:58:10.000Z",
      "redemption": {
        "redeemed_at": "2026-08-12T16:03:44.000Z",
        "pfc_account_name": "Alex Trader",
        "pfc_account_email": "trader+pfc@example.com"
      },
      "entitlement": {
        "plan": "monthly",
        "state": "monthly_active"
      },
      "pii_purged": false
    }
  ],
  "page": {
    "limit": 50,
    "has_more": true,
    "next_cursor": "eyJ2IjoxLCJhZnRlciI6Ii4uLiJ9..."
  }
}

next_cursor is null when has_more is false. An empty result is data: [], not 404. Pass the returned cursor in the next request using --data-urlencode "cursor=$CURSOR". Do not edit cursors or reuse them with another vendor or environment; an invalid cursor returns 400 VENDOR_PROMOTION_INVALID_REQUEST.

Redemption order is not update order. An older code whose license changes today stays on its original redemption page. Polling only the first page will miss those changes. Use webhooks or regularly refresh your saved code IDs through the status endpoint. To rebuild redeemed-code state after an outage, start without a cursor, follow every page until has_more is false, and apply only higher status_version values for each code.

03 / Delivery & operations

Webhooks#

Webhooks are the primary mechanism for learning that one of your codes was consumed or that its entitlement status changed. Delivery is signed and at-least-once: duplicates and out-of-order arrival are possible, so deduplicate by event ID and order by status_version. Webhooks are optional — status and redemption-list polling remain available when delivery is disabled or not configured.

Endpoint requirements#

Your endpoint must use a publicly trusted HTTPS certificate on port 443, accept POST, and must not redirect. URLs containing credentials, query strings, fragments, localhost, or literal IP addresses are rejected. PropFirmConnector resolves your hostname both at configuration time and immediately before every delivery, rejects any non-public address, and never follows redirects.

  1. Save the endpoint in Vendor Admin. Copy the pending signing key ID and the one-time signing secret.
  2. Install that pending key in your receiver before clicking Verify. The secret is displayed as base64:<encoded bytes>: remove the base64: prefix and decode the rest from base64 to bytes. Do not use the displayed text itself as the HMAC key.
  3. Click Verify. PropFirmConnector signs a vendor.webhook.challenge with the pending key. Verify its signature, then return 2xx JSON echoing the received nonce as {"challenge":"<received-value>"}.
  4. A successful challenge makes the pending URL and key current. Enable webhook delivery when your receiver is ready.

For a URL change or key rotation, keep accepting the current key while you install and verify the pending key. The active configuration continues delivering until the replacement is verified; new deliveries then use the new key.

Event types#

EventEmitted when
vendor.discount_code.redeemedThe first durable transition from unused to a consumed status.
vendor.entitlement.status_changedAny later status change, including the first paid transition, Monthly activity changes, a trial ending unpaid, or Lifetime activation.
vendor.payment.reversedA qualifying payment was refunded, reversed, or charged back and status became payment_reversed.

Ignore unknown event types after recording their event ID — additive event types do not require a new API version.

Payload#

Delivery structure · discount_code abbreviated
{
  "id": "evt_01K2Z2W6MV4BTJ5R3Z1P7Q8N9C",
  "type": "vendor.entitlement.status_changed",
  "api_version": "v1",
  "created_at": "2026-08-19T16:04:12Z",
  "environment": "live",
  "vendor_id": "your-vendor-id",
  "data": {
    "previous_status": "trial_active",
    "discount_code": { /* canonical discount_code object */ }
  }
}

The discount_code field contains the full object shown above. The raw customer-facing code and payment details are omitted. vendor_id identifies your vendor.

Signature verification#

Every delivery includes these headers:

Delivery headers
X-PFC-Webhook-Id: evt_01K2Z2W6MV4BTJ5R3Z1P7Q8N9C
X-PFC-Webhook-Timestamp: 1787155452
X-PFC-Webhook-Key-Id: whk_xxxxxxxxxxxxxxxx
Content-Digest: sha-256=:<standard-base64-sha256>:
X-PFC-Webhook-Signature: v1=<64-lowercase-hex-characters>
Content-Type: application/json

Compute Content-Digest as SHA-256 over the exact raw body bytes, encoded as standard padded base64 and wrapped as sha-256=:{digest}:. Build the canonical string by joining these four values with a single line-feed byte (0x0A) and no trailing line feed:

Canonical string
PFC-WEBHOOK-HMAC-SHA256-V1
{WEBHOOK_ID}
{WEBHOOK_TIMESTAMP}
{CONTENT_DIGEST_HEADER_VALUE}

Select the decoded secret bytes named by X-PFC-Webhook-Key-Id, compute HMAC-SHA256 over the canonical string, encode as 64 lowercase hex characters, prefix with v1=, and compare the complete header value in constant time. The key ID is a selector only — it is not an additional canonical-string line. After a rotation is promoted, PropFirmConnector signs only with the newly verified key.

Node.js verifier

Save this as pfc-webhook.cjs. It accepts an untouched body Buffer and Node's lowercase req.headers. Capture the body before any JSON parser runs; parsing and re-serializing JSON changes the signed bytes.

pfc-webhook.cjs · Node.js
const { createHash, createHmac, timingSafeEqual } = require('node:crypto');

function decodeSigningSecret(displayedSecret) {
  if (typeof displayedSecret !== 'string' || !displayedSecret.startsWith('base64:')) {
    throw new Error('Use the base64: signing secret displayed in Vendor Admin.');
  }
  const encoded = displayedSecret.slice(7);
  const secret = Buffer.from(encoded, 'base64');
  if (secret.length < 32 || secret.toString('base64') !== encoded) {
    throw new Error('Invalid signing secret encoding.');
  }
  return secret;
}

function equalText(actual, expected) {
  const a = Buffer.from(actual, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  return a.length === b.length && timingSafeEqual(a, b);
}

// headers is Node's req.headers; signingKeys maps key IDs to decoded Buffers.
function verifyPfcWebhook(rawBody, headers, signingKeys,
  nowSeconds = Math.floor(Date.now() / 1000)) {
  if (!Buffer.isBuffer(rawBody)) throw new Error('An untouched body Buffer is required.');
  const header = (name) => {
    const value = headers[name];
    if (typeof value !== 'string' || !value) throw new Error('Missing or repeated header.');
    return value;
  };
  const id = header('x-pfc-webhook-id');
  const timestamp = header('x-pfc-webhook-timestamp');
  const keyId = header('x-pfc-webhook-key-id');
  const digest = header('content-digest');
  const signature = header('x-pfc-webhook-signature');
  if (!/^\d{1,12}$/.test(timestamp) || !Number.isFinite(nowSeconds) ||
      Math.abs(nowSeconds - Number(timestamp)) > 300) {
    throw new Error('Webhook timestamp outside the allowed window.');
  }
  const secret = signingKeys.get(keyId);
  if (!Buffer.isBuffer(secret) || secret.length < 32) throw new Error('Unknown signing key.');
  const expectedDigest = 'sha-256=:' + createHash('sha256').update(rawBody).digest('base64') + ':';
  if (!equalText(digest, expectedDigest)) throw new Error('Body digest mismatch.');
  const canonical = ['PFC-WEBHOOK-HMAC-SHA256-V1', id, timestamp, digest].join('\n');
  const expectedSignature = 'v1=' + createHmac('sha256', secret).update(canonical, 'utf8').digest('hex');
  if (!/^v1=[a-f0-9]{64}$/.test(signature) || !equalText(signature, expectedSignature)) {
    throw new Error('Invalid webhook signature.');
  }
  const payload = JSON.parse(rawBody.toString('utf8'));
  if (!payload || payload.id !== id) throw new Error('Event ID mismatch.');
  return payload;
}

module.exports = { decodeSigningSecret, verifyPfcWebhook };
Use in your receiver
const { decodeSigningSecret, verifyPfcWebhook } = require('./pfc-webhook.cjs');
const signingKeys = new Map([
  [process.env.PFC_WEBHOOK_KEY_ID, decodeSigningSecret(process.env.PFC_WEBHOOK_SECRET)],
]);
// Add the pending key to this map before verifying a new endpoint or rotating keys.
const event = verifyPfcWebhook(rawBody, req.headers, signingKeys);

Catch verification errors and reject the request without processing it. For a verified challenge, return the challenge echo. For normal events, follow the receiver checklist before acknowledging.

Verification test vector#

Use this fixed vector to check your implementation. Its test secret is the ASCII byte string api-signing-secret-for-unit-tests-000001 and the exact body is {"id":"event-one","status":"monthly_active"}:

With the verifier above, use Buffer.from('api-signing-secret-for-unit-tests-000001', 'ascii') for the whk_test_vector map entry and pass 1700000000 as nowSeconds. Production secrets must instead go through decodeSigningSecret; never override the clock in your live receiver.

Expected values
Content-Digest: sha-256=:omCmGngHElJyVgKGRI5gHCUSYUVwxWt6upHJACSG+ao=:
X-PFC-Webhook-Id: event-one
X-PFC-Webhook-Timestamp: 1700000000
X-PFC-Webhook-Key-Id: whk_test_vector
X-PFC-Webhook-Signature: v1=aaf3f4e5151d3933484955db5a3ad433c367a2c9f65d63c11fa8aee536d4b738
Its canonical string
PFC-WEBHOOK-HMAC-SHA256-V1
event-one
1700000000
sha-256=:omCmGngHElJyVgKGRI5gHCUSYUVwxWt6upHJACSG+ao=:

Receiver checklist#

  1. Resolve the key ID from your current or pending verification keys, then verify the signature under that exact key.
  2. The timestamp is no more than 300 seconds from your current time.
  3. The header event ID exactly equals the JSON id.
  4. Handle verified challenges separately; do not treat them as customer events.

Save a verified event to a durable queue before returning success, or save its event ID and status update in one database transaction. Do not mark an event processed before its update is saved. For each discount_code_id, apply only a higher status_version. Return success for duplicates and older versions without repeating credits, notifications, or other side effects.

Acknowledgment and retries#

Return any 2xx within four seconds; normal event response bodies are ignored (challenge deliveries are the exception and must echo the challenge). Network failures, timeouts, and every non-2xx response are retried: the dispatcher scans for due deliveries approximately every five minutes, the retry delay doubles from 30 seconds up to a 24-hour cap, and delivery stops after 12 total attempts. Retry-After is not honored in v1.

The vendor portal provides a synthetic test delivery, recent delivery summaries, enable/disable controls, staged signing-secret rotation, and replay for delivered or retry-exhausted events. Replays retain the original event ID and remain subject to your normal deduplication. After an extended outage, reconcile through the status and redemption-list endpoints.

Errors#

Every non-2xx API response uses this envelope when a JSON response can be produced:

Error envelope
{
  "error": "The request body must be an empty JSON object.",
  "code": "VENDOR_PROMOTION_INVALID_REQUEST"
}
HTTPcodeRetry?
400VENDOR_PROMOTION_INVALID_REQUESTNo — correct the request.
401VENDOR_AUTHENTICATION_FAILEDNo automatic retry until the bearer credential, environment, state, and scope are checked.
404VENDOR_PROMOTION_NOT_FOUNDNo. A resource outside your vendor's scope is also not found.
409VENDOR_PROMOTION_CONFLICTNo for an idempotency-key/body conflict; reconcile before any new create key.
413VENDOR_REQUEST_TOO_LARGENo.
422VENDOR_PROMOTION_UNAVAILABLENo without a new eligible code or resolved checkout state.
429VENDOR_RATE_LIMITEDYes, with client backoff.
503VENDOR_PROMOTION_SERVICE_UNAVAILABLEYes, idempotently.

Messages are safe for an operator but not intended for direct customer display. Authentication errors deliberately reveal no failing component, and validation errors never echo secrets or the raw request body.

Rate limits & retries#

Initial per-vendor limits, shared by operation category within your vendor and environment (rotating keys cannot create a fresh bucket):

OperationLimit
Create10 requests per minute
Status120 requests per minute
Redeemed list60 requests per minute

A rolling per-vendor issuance quota also applies; details are provided during onboarding. Limits may be raised by agreement or reduced temporarily for abuse protection without changing the API version. Rate-limit response headers are not part of v1.

Recommended client retry policy:

  1. Use connection and response timeouts.
  2. Retry up to five times with exponential backoff and full jitter.
  3. For GET, retry 408, 429, 500, 502, 503, and 504. For create, retry the same statuses only with the original body and Idempotency-Key. Use a valid key for the same vendor and environment; rotating the bearer key does not require a new idempotency key.
  4. Apply local backoff on 429 and 503; v1 does not promise a Retry-After header.
  5. Stop on other 4xx responses.
  6. If all create retries fail, search your operational logs by idempotency key or contact PropFirmConnector before issuing a different key.

Data retention & security#

RecordRetention
Pending API credential24 hours unless activated or revoked sooner
Pending webhook endpoint / signing secretUsable for 24 hours unless verified or replaced sooner
Idempotency records and stored create responses12 months
Expired or revoked unused code record12 months after creation, so the create-idempotency guarantee stays verifiable
Redeemed record, including the name/email snapshotsWhile the entitlement is active, then 12 months after it becomes inactive
Webhook event body and delivery-attempt detail90 days from event occurrence
API security/access audit events90 days
Webhook administration audit events90 days

For retention, trial_active, monthly_active, and lifetime are active; trial_ended_unpaid, monthly_inactive, and payment_reversed are inactive and start the 12-month clock. If a Monthly record returns to active before deletion, the clock stops and restarts the next time it becomes inactive. At the end of the window the name and email snapshots are deleted or irreversibly anonymized and the canonical object reports pii_purged: true; the non-PII code and status record remains available for reconciliation. A targeted legal or dispute hold may suspend deletion only for the affected record.

Bearer credentials and customer-facing codes are stored only as one-way hashes after creation. Bearer credentials, raw codes, full request bodies, and payment details are excluded from application logs, and profile names and emails are never written to access logs. Data is encrypted in transit and at rest, and credential-derived tenant access lets a vendor read only its own records in the authenticated environment.

You are responsible for protecting the create response and all API and webhook data, limiting employee access, and encrypting stored credentials and customer mappings. Delete the name/email snapshots no later than 12 months after the entitlement becomes inactive unless a legal obligation requires a narrower targeted hold, and report any suspected credential, code, or PII leak promptly so the affected key or webhook secret can be revoked.

04 / Launch

Go-live#

Onboarding is direct to production: once your offer is configured and your production credentials are issued, the codes you create are live. Production enablement requires secret-storage confirmation and at least one enrolled administrator in the vendor portal. Confirmed webhook verification (the challenge echo) is required only before you enable webhook delivery — polling may launch without it. PropFirmConnector's checkout tells the redeeming customer what is shared with your firm before the code is consumed.

Before relying on your integration, verify on your side that:

  1. If using webhooks, your receiver validates the published byte-exact signature test vector locally — no live delivery is needed to prove your verification code.
  2. Your create path retries a lost response with the same Idempotency-Key and never re-keys.
  3. If using webhooks, your receiver deduplicates by event ID and applies updates in status_version order, returning success for duplicates.
  4. Your reconciliation path can rebuild state from the status and redemption-list endpoints after an outage.

Coordinate your first issued code with PropFirmConnector so both sides confirm redemption and status reads end to end, plus webhook delivery if enabled.

Versioning#

v1 is a stable contract: additive response fields and new webhook event types may appear at any time and must be tolerated; unknown request fields are always rejected; breaking changes require a new major path such as /vendor/v2. Webhook signature changes require a new explicit protocol marker and an overlap plan.

Getting access#

Configure your coupon offer

Email your firm name and website to support@propfirmconnector.com. We agree on allowed plans, discounts and code expiry, then arrange administrator access and your first live redemption.

Open Vendor Admin · Documentation index

Authenticator verification

Enter your authentication code

Enter the current 6-digit code from your authenticator app.