Skip to content

OAuth API

The LecturePanda API lets an external application read data from a LecturePanda organization on that organization's behalf. Access is granted by an administrator through an OAuth 2.0 consent screen — you never handle a LecturePanda password — and every request is authenticated with a bearer token scoped to a single organization.

Base URL: https://www.lecturepanda.com

All API endpoints live under /api/v1. The OAuth endpoints live under /auth.

PurposeMethodPath
Authorization requestGET/auth/authorize
Token / refreshPOST/auth/token
Organization detailsGET/api/v1/organization
Registration typesGET/api/v1/registration-types
Registration fieldsGET/api/v1/registration-fields
Recent registrationsGET/api/v1/recent/registrations
Recent completionsGET/api/v1/recent/completions

Getting client credentials

OAuth clients are registered by LecturePanda staff. Contact support to request a client and provide:

  • Application name, logo URL, homepage URL, privacy policy URL, and terms of service URL — these are shown to the administrator on the consent screen.
  • Redirect URIs — the exact, full URIs your application will use. A redirect URI supplied at authorization time must match one of the registered values character for character.
  • Scopes your application needs (see Scopes).
  • Whether you want PKCE required for your client.

You will receive a client_id and a client_secret. The secret is only usable from your server — do not ship it in a browser or mobile app.

Authorization code flow

LecturePanda supports the OAuth 2.0 authorization code grant (response_type=code, grant_type=authorization_code) with refresh tokens. It is the only supported grant.

Step 1 — Send the administrator to the authorization endpoint

GET https://www.lecturepanda.com/auth/authorize
  ?client_id=YOUR_CLIENT_ID
  &response_type=code
  &redirect_uri=https%3A%2F%2Fyourapp.example.com%2Foauth%2Fcallback
  &scope=organization%3Aread%20registrations%3Aread%20lectures%3Aread
  &state=RANDOM_OPAQUE_STRING
ParameterRequiredDescription
client_idYesYour client identifier.
response_typeYesMust be code.
redirect_uriNoMust exactly match a registered redirect URI. If omitted, the client's default redirect URI is used.
scopeNoSpace-separated scopes. Must be a subset of the scopes your client is registered for. Defaults to the client's default scopes.
stateNoOpaque value returned unchanged on the redirect. Strongly recommended for CSRF protection.
code_challengeNo*PKCE challenge. Required if your client is registered with PKCE required.
code_challenge_methodNo*Use S256.

What the administrator sees:

  1. If they are not signed in to LecturePanda, they are sent to the login page first and returned to the authorization request afterward.
  2. The consent screen shows your application name, logo, and a plain-language description of each requested scope, plus links to your privacy policy and terms of service.
  3. They choose which organization to connect. If they administer more than one organization, a picker is shown; the resulting token is bound to that single organization. Users who are not administrators of any organization see an error and cannot continue.
  4. On approval, the browser is redirected to your redirect_uri with code and state.
https://yourapp.example.com/oauth/callback?code=AUTHORIZATION_CODE&state=RANDOM_OPAQUE_STRING

WARNING

Authorization codes are single use. Exchange the code promptly — a code that has already been redeemed is rejected.

Errors that can be attributed to a valid client and redirect URI are returned as standard OAuth error parameters on the redirect. Errors that cannot (unknown client_id, unregistered redirect_uri) render an error page with HTTP 401 instead of redirecting.

Step 2 — Exchange the code for tokens

http
POST /auth/token HTTP/1.1
Host: www.lecturepanda.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https%3A%2F%2Fyourapp.example.com%2Foauth%2Fcallback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

Client credentials may be sent in the form body as shown, or as HTTP Basic authentication. The redirect_uri must match the one used in the authorization request.

json
{
  "access_token": "0Xf2h9...",
  "expires_in": 3600,
  "token_type": "Bearer",
  "scope": "organization:read registrations:read lectures:read",
  "refresh_token": "9dK1p3..."
}

Token endpoint failures follow RFC 6749 — an HTTP 400 with a JSON body such as {"error": "invalid_grant"} or {"error": "invalid_client"}.

Step 3 — Call the API

Send the access token as a bearer token on every request:

bash
curl -H "Authorization: Bearer 0Xf2h9..." \
  "https://www.lecturepanda.com/api/v1/organization"

Step 4 — Refresh

http
POST /auth/token HTTP/1.1
Host: www.lecturepanda.com
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=9dK1p3...
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

The response has the same shape as the initial token response.

Refresh tokens rotate

Every refresh issues a new access token and a new refresh token, and replaces the stored token record. Persist both values from every token response and discard the old ones. The previous access token stops working once a refresh succeeds.

An optional scope parameter may be supplied on refresh, but it must be a subset of the scopes originally granted. Omit it to keep the original scopes.

Token lifetime

Treat expires_in (seconds) from the token response as authoritative and refresh before it elapses. Also refresh and retry once on any 401, which is what an expired or revoked token looks like from the API's perspective.

PKCE

If your client is registered with PKCE required, include code_challenge and code_challenge_method=S256 on the authorization request and code_verifier on the token request. PKCE is optional for confidential clients that can protect a client secret.

Scopes

Scopes are named <resource>:<action>. The action is read or write; every endpoint documented here is a GET and therefore needs only :read scopes.

ScopeShown to the administrator as
organization:readView basic information about your organization.
registrations:readView participant registration details.
registrations:writeCreate/Update participant registrations.
lectures:readView lecture details.
lectures:writeCreate/Update lectures.
payments:readView non-sensitive payment details.
completions:readView credit completion data.

Endpoints that return data joined from several resources require all of the corresponding scopes:

EndpointRequired scopes
/api/v1/organizationorganization:read
/api/v1/registration-typesorganization:read
/api/v1/registration-fieldsorganization:read
/api/v1/recent/registrationsregistrations:read, lectures:read, payments:read
/api/v1/recent/completionsregistrations:read, lectures:read, completions:read

A token missing any required scope for an endpoint is rejected with 401.

INFO

The :write scopes are defined and can be granted, but no write endpoints are published yet.

Conventions

Authentication

Every /api/v1 request requires Authorization: Bearer <access_token>. There is no anonymous access, and no API-key alternative.

Organization context

A token is bound to the one organization the administrator selected during consent. Every endpoint returns data for that organization only — there is no organization parameter, and there is no way to reach another organization with the same token.

List envelope

List endpoints return an envelope:

json
{
  "data": [],
  "start_cursor": null,
  "end_cursor": "CkQKDmRhdGV0aW1lc3RhbXAS...",
  "hasmore": true
}
FieldDescription
dataArray of result objects.
start_cursorThe cursor supplied on this request, or null for the first page.
end_cursorCursor to pass as cursor to fetch the next page.
hasmoretrue when another page may exist.

/api/v1/organization is the exception — it returns a single object under an organization key.

Pagination

Pages hold up to 50 records. Only /recent/registrations and /recent/completions paginate; the organization, registration type, and registration field endpoints return the full set in one response.

Loop until hasmore is false, passing the previous end_cursor:

js
async function fetchAll(url, params, token) {
  const results = [];
  let cursor = null;
  let hasmore = true;

  while (hasmore) {
    const query = new URLSearchParams({
      ...params,
      ...(cursor ? { cursor } : {}),
    });
    const res = await fetch(`${url}?${query}`, {
      headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const page = await res.json();
    results.push(...page.data);
    cursor = page.end_cursor;
    hasmore = page.hasmore;
  }
  return results;
}

Cursors are opaque and short-lived — use them to walk one result set, not to bookmark a position between polls. Use request_datetime for that (see Polling).

Field conventions

  • key — the globally unique, URL-safe identifier for a record. Use this when you need a stable identifier.
  • id — the numeric portion of the key. Unique within a kind, convenient for display.
  • Dates and times — ISO 8601 in UTC, e.g. "2023-04-06T15:40:09.992582Z".
  • Moneyamount on a charge is an integer in cents; display_amount is a preformatted string like "$220.00".
  • Related records — related objects are inlined as nested objects (for example, lecturekey on a registration is the lecture object, not a key string), trimmed to the fields listed for each endpoint.

Errors

StatusMeaning
400Invalid query parameter — a missing required parameter, a malformed date, or a bad cursor. The reason is in the response body.
401Missing, malformed, expired, or revoked access token, or a token lacking a required scope for the endpoint.
403Returned during the consent flow when the selected organization is not one the signed-in user administers.

API errors return the status code with a short text/HTML body rather than a JSON error object — branch on the status code, not the body. Token endpoint errors are the exception and return JSON as described above.

Endpoints

Organization

GET /api/v1/organization

Scope: organization:read. Useful as a connection test and for labeling the connection in your UI.

json
{
  "organization": {
    "key": "ag1zfnJ4Y2V0cmFja2Vycg0LEgZDRVVzZXIY-wUM",
    "id": 763,
    "companyname": "Example Continuing Education",
    "getUserName": "admin@example.org"
  }
}
FieldTypeDescription
key, idstring, numberIdentifiers for the connected organization.
companynamestringOrganization display name.
getUserNamestringPrimary account login (email) for the organization.

Registration types

GET /api/v1/registration-types

Scope: organization:read. Returns every active registration type available to the organization, including shared types provided by LecturePanda. Returned in the standard list envelope with no pagination.

json
{
  "data": [
    {
      "key": "ag1zfnJ4Y2V0cmFja2VyciQLEgZDRVVzZXIY-wUMCxIQUmVnaXN0cmF0aW9uVHlwZRj1Bww",
      "id": 1013,
      "name": "member",
      "label": "Member Rate",
      "createdate": "2022-11-02T14:21:07.201000Z",
      "regfields": [
        "ag1zfnJ4Y2V0cmFja2VyciULEgZDRVVzZXIY-wUMCxIRUmVnaXN0cmF0aW9uRmllbGQY7Q0M"
      ],
      "requiredfields": [
        "ag1zfnJ4Y2V0cmFja2VyciULEgZDRVVzZXIY-wUMCxIRUmVnaXN0cmF0aW9uRmllbGQY7Q0M"
      ]
    }
  ],
  "start_cursor": null,
  "end_cursor": null,
  "hasmore": false
}
FieldTypeDescription
namestringInternal name.
labelstringName shown to participants.
createdatedatetimeWhen the type was created.
regfieldsarrayKeys of the registration fields collected for this type.
requiredfieldsarrayKeys of the registration fields that are required.

Match these keys against the key values from Registration fields.

Registration fields

GET /api/v1/registration-fields

Scope: organization:read. Returns the custom participant fields available to the organization. Returned in the standard list envelope with no pagination.

json
{
  "data": [
    {
      "key": "ag1zfnJ4Y2V0cmFja2VyciULEgZDRVVzZXIY-wUMCxIRUmVnaXN0cmF0aW9uRmllbGQY7Q0M",
      "id": 1773,
      "name": "meal_choice",
      "label": "Meal Choice",
      "field_name": "fld1773",
      "fieldtype": "select",
      "choices": ["Vegetarian", "Chicken", "Fish"],
      "mask": null,
      "regexvalidate": null,
      "regexerrormessage": null,
      "helpText": "Choose your meal for the luncheon."
    }
  ],
  "start_cursor": null,
  "end_cursor": null,
  "hasmore": false
}
FieldTypeDescription
namestringInternal name.
labelstringPrompt shown to the participant. This is the key used in custom_field_data on a registration.
field_namestringInternal storage name (fld<id>).
fieldtypestringtext or select.
choicesarrayAllowed values when fieldtype is select.
maskstringInput mask, if configured.
regexvalidatestringValidation pattern, if configured.
regexerrormessagestringMessage shown when validation fails.
helpTextstringHelper text shown under the field.

Recent registrations

GET /api/v1/recent/registrations

Scopes: registrations:read, lectures:read, payments:read.

Returns active registrations created at or after request_datetime - look_back_minutes, in the standard paginated envelope. Results are not sorted chronologically — read datetimestamp on each record rather than relying on position.

ParameterRequiredDefaultDescription
request_datetimeYesISO 8601 timestamp. Include an offset (or Z) — a value without one is interpreted in the server's local time zone.
look_back_minutesNo30Integer. How far back from request_datetime to include.
cursorNoend_cursor from a previous page.
bash
curl -H "Authorization: Bearer $TOKEN" \
  "https://www.lecturepanda.com/api/v1/recent/registrations?request_datetime=2023-04-06T16:00:00Z&look_back_minutes=20"
json
{
  "data": [
    {
      "key": "ag1zfnJ4Y2V0cmFja2VychMLEgxSZWdpc3RyYXRpb24YhRIM",
      "id": 2309,
      "name": "Jeffery Williams",
      "first_name": "Jeffery",
      "last_name": "Williams",
      "email": "edward79@example.org",
      "datetimestamp": "2023-04-06T15:40:09.992582Z",
      "memberID": "10045",
      "custom_field_data": {
        "Company Name": "Example Co",
        "Phone Number": "555-555-5555"
      },
      "regtype": {
        "key": "ag1zfnJ4Y2V0cmFja2VyciQLEgZDRVVzZXIY-wUMCxIQUmVnaXN0cmF0aW9uVHlwZRj1Bww",
        "id": 1013,
        "label": "Member Rate"
      },
      "lecturekey": {
        "key": "ag1zfnJ4Y2V0cmFja2VychsLEgZDRVVzZXIY-wUMCxIHTGVjdHVyZRiOCAw",
        "id": 1038,
        "title": "Managing Chronic Pain",
        "startdatetime": "2023-04-04T11:39:49.271345Z",
        "stopdatetime": "2023-04-06T12:01:40Z",
        "timezone": "America/Detroit",
        "location": "Room 204",
        "address": "9693 Angela Squares, East Elizabethborough, SC 94277",
        "categories": []
      },
      "chargerecord": {
        "key": "ag1zfnJ4Y2V0cmFja2VychMLEgxTdHJpcGVDaGFyZ2UYhhIM",
        "id": 2310,
        "chargeid": "60214080316",
        "amount": 22000,
        "display_amount": "$220.00",
        "gateway": "authorizenet",
        "datetimestamp": "2023-04-06T15:40:10.351926Z",
        "marketplace": false,
        "gateway_used": {
          "key": "ag1zfnJ4Y2V0cmFja2Vych0LEhZBdXRob3JpemVOZXRDb25uZWN0aW9uGNkNDA",
          "id": 1753,
          "display_name": "Authorize.Net Test Account",
          "display_type": "Authorize.Net"
        },
        "pricing_rule": {
          "key": "ag1zfnJ4Y2V0cmFja2VychILEgtQYXltZW50UnVsZRiEDgw",
          "id": 1796,
          "description": "Member Early Bird",
          "price": "220"
        }
      },
      "purchased_addons": [
        {
          "key": "ag1zfnJ4Y2V0cmFja2VychQLEg1BZGRPblByb2R1Y3QYjgkM",
          "id": 1166,
          "name": "Printed Handbook",
          "price": "15"
        }
      ]
    }
  ],
  "start_cursor": null,
  "end_cursor": "CkQKDmRhdGV0aW1lc3RhbXAS...",
  "hasmore": true
}
FieldTypeDescription
namestringParticipant's full name as entered.
first_name, last_namestringParsed from name.
emailstringParticipant email.
datetimestampdatetimeWhen the registration was created.
memberIDstringMember identifier from the connected membership system, when one is matched.
custom_field_dataobjectCustom field values keyed by the field's label.
regtypeobjectRegistration type (label).
lecturekeyobjectLecture the participant registered for.
chargerecordobjectPayment record, or absent for free registrations.
chargerecord.amountintegerAmount charged, in cents.
chargerecord.gatewaystringProcessor slug, e.g. stripe, authorizenet.
chargerecord.marketplacebooleanWhether the charge went through the LecturePanda marketplace.
chargerecord.gateway_usedobjectThe specific connected payment account used.
chargerecord.pricing_ruleobjectThe pricing rule that set the price.
purchased_addonsarrayAdd-on products purchased with the registration.

INFO

Only active registrations are returned. A registration that is later cancelled simply stops appearing — this endpoint reports new activity, so use it to add records, not to detect deletions.

Recent completions

GET /api/v1/recent/completions

Scopes: registrations:read, lectures:read, completions:read.

Returns credit reporting records — one per credit reported for a participant — created at or after request_datetime - look_back_minutes, newest first.

ParameterRequiredDefaultDescription
request_datetimeYesISO 8601 timestamp. Include an offset (or Z).
successYestrue or false. Filters to successful or failed credit reports.
look_back_minutesNo30Integer. How far back from request_datetime to include.
cursorNoend_cursor from a previous page.
bash
curl -H "Authorization: Bearer $TOKEN" \
  "https://www.lecturepanda.com/api/v1/recent/completions?request_datetime=2023-04-17T18:30:00Z&look_back_minutes=20&success=true"
json
{
  "data": [
    {
      "key": "ag1zfnJ4Y2V0cmFja2VychwLEhVDcmVkaXRSZXBvcnRpbmdSZWNvcmQY8xgM",
      "id": 3187,
      "action": "Completion",
      "datetimestamp": "2023-04-17T18:19:12.457362Z",
      "success": true,
      "completed": true,
      "completion_message": null,
      "error_messages": [],
      "error_ids": [],
      "reported_data": null,
      "owner_key": "ag1zfnJ4Y2V0cmFja2Vycg0LEgZDRVVzZXIY-wUM",
      "regkey": {
        "key": "ag1zfnJ4Y2V0cmFja2VychMLEgxSZWdpc3RyYXRpb24Y2BgM",
        "id": 3160,
        "name": "Frank Ramos",
        "first_name": "Frank",
        "last_name": "Ramos",
        "email": "framos@example.org",
        "datetimestamp": "2023-04-16T16:03:43.930121Z",
        "memberID": "10045",
        "external": null,
        "custom_field_data": {
          "Meal Choice": "Vegetarian"
        },
        "regtype": {
          "key": "ag1zfnJ4Y2V0cmFja2VyciQLEgZDRVVzZXIY-wUMCxIQUmVnaXN0cmF0aW9uVHlwZRiCCAw",
          "id": 1026,
          "label": "Member Rate"
        }
      },
      "creditkey": {
        "key": "ag1zfnJ4Y2V0cmFja2VycisLEgZDRVVzZXIY-wUMCxIHTGVjdHVyZRjmFQwLEglDcmVkaXREZWYY1xYM",
        "id": 2903,
        "name": "Pharmacy Law CE",
        "cehours": 1
      },
      "accreditationkey": {
        "key": "ag1zfnJ4Y2V0cmFja2VychQLEg1BY2NyZWRpdGF0aW9uGI4JDA",
        "id": 1166,
        "name": "Example CE Broker",
        "accreditation_type": "cebroker"
      },
      "lectkey": {
        "key": "ag1zfnJ4Y2V0cmFja2VychsLEgZDRVVzZXIY-wUMCxIHTGVjdHVyZRjmFQw",
        "id": 2790,
        "title": "Pharmacy Law Update",
        "startdatetime": "2023-04-04T11:40:45.553000Z",
        "stopdatetime": "2023-04-08T11:40:45.553000Z",
        "timezone": "America/Detroit",
        "location": "Auditorium",
        "address": "069 Garcia Extension Suite 763, East Curtisborough, AL 74432",
        "categories": []
      }
    }
  ],
  "start_cursor": null,
  "end_cursor": "CkQKDmRhdGV0aW1lc3RhbXAS...",
  "hasmore": false
}
FieldTypeDescription
actionstringType of event, e.g. Completion.
datetimestampdatetimeWhen the credit report was attempted.
successbooleanWhether the report succeeded. Mirrors the success filter.
completedbooleanWhether the credit is considered complete for the participant.
completion_messagestringMessage recorded with the completion, when present.
error_messagesarrayHuman-readable reasons a failed report failed.
error_idsarrayAccreditor error codes for a failed report.
reported_dataobjectRaw payload sent to the accreditor, when recorded.
owner_keystringKey of the organization the record belongs to.
regkeyobjectParticipant registration, including external (data from a connected external system).
creditkeyobjectThe credit itself — name and cehours (contact hours).
accreditationkeyobjectWhere the credit was reported, e.g. CE Broker or CPE Monitor.
lectkeyobjectLecture the credit belongs to.

One participant can produce several records

A completion record is written per credit, per reporting destination. A participant who claims two credits, or one credit reported to two accreditors, produces multiple records with the same regkey. Deduplicate on key and, if you only want one event per participant, group by regkey.key before acting.

Polling for new activity

The recent endpoints are designed for polling on a fixed interval. The reliable pattern is a look-back window slightly longer than your polling interval, so a slow request or clock skew cannot open a gap:

  1. Poll every N minutes.
  2. Send request_datetime as the current time in ISO 8601 with a Z or offset.
  3. Send look_back_minutes as roughly N + 50% (for example, look_back_minutes=20 for a 15-minute poll).
  4. Page through the result set with end_cursor until hasmore is false.
  5. Deduplicate against records you have already processed, using key.

The overlap means you will see each record more than once — step 5 is not optional.

js
const now = new Date().toISOString();
const registrations = await fetchAll(
  "https://www.lecturepanda.com/api/v1/recent/registrations",
  { request_datetime: now, look_back_minutes: 20 },
  accessToken,
);

TIP

The window is anchored on request_datetime, not on "now" — records created after request_datetime are also returned. To backfill, walk request_datetime backwards in steps and set look_back_minutes to the step size.

Revoking access

An administrator can disconnect your application from their LecturePanda account at any time, and LecturePanda can deactivate a client. In both cases existing tokens stop working and requests return 401. Handle that by prompting the administrator to reconnect through the authorization flow rather than retrying indefinitely.