Appearance
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.
| Purpose | Method | Path |
|---|---|---|
| Authorization request | GET | /auth/authorize |
| Token / refresh | POST | /auth/token |
| Organization details | GET | /api/v1/organization |
| Registration types | GET | /api/v1/registration-types |
| Registration fields | GET | /api/v1/registration-fields |
| Recent registrations | GET | /api/v1/recent/registrations |
| Recent completions | GET | /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| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Your client identifier. |
response_type | Yes | Must be code. |
redirect_uri | No | Must exactly match a registered redirect URI. If omitted, the client's default redirect URI is used. |
scope | No | Space-separated scopes. Must be a subset of the scopes your client is registered for. Defaults to the client's default scopes. |
state | No | Opaque value returned unchanged on the redirect. Strongly recommended for CSRF protection. |
code_challenge | No* | PKCE challenge. Required if your client is registered with PKCE required. |
code_challenge_method | No* | Use S256. |
What the administrator sees:
- If they are not signed in to LecturePanda, they are sent to the login page first and returned to the authorization request afterward.
- 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.
- 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.
- On approval, the browser is redirected to your
redirect_uriwithcodeandstate.
https://yourapp.example.com/oauth/callback?code=AUTHORIZATION_CODE&state=RANDOM_OPAQUE_STRINGWARNING
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_SECRETClient 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_SECRETThe 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.
| Scope | Shown to the administrator as |
|---|---|
organization:read | View basic information about your organization. |
registrations:read | View participant registration details. |
registrations:write | Create/Update participant registrations. |
lectures:read | View lecture details. |
lectures:write | Create/Update lectures. |
payments:read | View non-sensitive payment details. |
completions:read | View credit completion data. |
Endpoints that return data joined from several resources require all of the corresponding scopes:
| Endpoint | Required scopes |
|---|---|
/api/v1/organization | organization:read |
/api/v1/registration-types | organization:read |
/api/v1/registration-fields | organization:read |
/api/v1/recent/registrations | registrations:read, lectures:read, payments:read |
/api/v1/recent/completions | registrations: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
}| Field | Description |
|---|---|
data | Array of result objects. |
start_cursor | The cursor supplied on this request, or null for the first page. |
end_cursor | Cursor to pass as cursor to fetch the next page. |
hasmore | true 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". - Money —
amounton a charge is an integer in cents;display_amountis a preformatted string like"$220.00". - Related records — related objects are inlined as nested objects (for example,
lecturekeyon a registration is the lecture object, not a key string), trimmed to the fields listed for each endpoint.
Errors
| Status | Meaning |
|---|---|
400 | Invalid query parameter — a missing required parameter, a malformed date, or a bad cursor. The reason is in the response body. |
401 | Missing, malformed, expired, or revoked access token, or a token lacking a required scope for the endpoint. |
403 | Returned 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/organizationScope: 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"
}
}| Field | Type | Description |
|---|---|---|
key, id | string, number | Identifiers for the connected organization. |
companyname | string | Organization display name. |
getUserName | string | Primary account login (email) for the organization. |
Registration types
GET /api/v1/registration-typesScope: 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
}| Field | Type | Description |
|---|---|---|
name | string | Internal name. |
label | string | Name shown to participants. |
createdate | datetime | When the type was created. |
regfields | array | Keys of the registration fields collected for this type. |
requiredfields | array | Keys of the registration fields that are required. |
Match these keys against the key values from Registration fields.
Registration fields
GET /api/v1/registration-fieldsScope: 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
}| Field | Type | Description |
|---|---|---|
name | string | Internal name. |
label | string | Prompt shown to the participant. This is the key used in custom_field_data on a registration. |
field_name | string | Internal storage name (fld<id>). |
fieldtype | string | text or select. |
choices | array | Allowed values when fieldtype is select. |
mask | string | Input mask, if configured. |
regexvalidate | string | Validation pattern, if configured. |
regexerrormessage | string | Message shown when validation fails. |
helpText | string | Helper text shown under the field. |
Recent registrations
GET /api/v1/recent/registrationsScopes: 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.
| Parameter | Required | Default | Description |
|---|---|---|---|
request_datetime | Yes | — | ISO 8601 timestamp. Include an offset (or Z) — a value without one is interpreted in the server's local time zone. |
look_back_minutes | No | 30 | Integer. How far back from request_datetime to include. |
cursor | No | — | end_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
}| Field | Type | Description |
|---|---|---|
name | string | Participant's full name as entered. |
first_name, last_name | string | Parsed from name. |
email | string | Participant email. |
datetimestamp | datetime | When the registration was created. |
memberID | string | Member identifier from the connected membership system, when one is matched. |
custom_field_data | object | Custom field values keyed by the field's label. |
regtype | object | Registration type (label). |
lecturekey | object | Lecture the participant registered for. |
chargerecord | object | Payment record, or absent for free registrations. |
chargerecord.amount | integer | Amount charged, in cents. |
chargerecord.gateway | string | Processor slug, e.g. stripe, authorizenet. |
chargerecord.marketplace | boolean | Whether the charge went through the LecturePanda marketplace. |
chargerecord.gateway_used | object | The specific connected payment account used. |
chargerecord.pricing_rule | object | The pricing rule that set the price. |
purchased_addons | array | Add-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/completionsScopes: 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.
| Parameter | Required | Default | Description |
|---|---|---|---|
request_datetime | Yes | — | ISO 8601 timestamp. Include an offset (or Z). |
success | Yes | — | true or false. Filters to successful or failed credit reports. |
look_back_minutes | No | 30 | Integer. How far back from request_datetime to include. |
cursor | No | — | end_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
}| Field | Type | Description |
|---|---|---|
action | string | Type of event, e.g. Completion. |
datetimestamp | datetime | When the credit report was attempted. |
success | boolean | Whether the report succeeded. Mirrors the success filter. |
completed | boolean | Whether the credit is considered complete for the participant. |
completion_message | string | Message recorded with the completion, when present. |
error_messages | array | Human-readable reasons a failed report failed. |
error_ids | array | Accreditor error codes for a failed report. |
reported_data | object | Raw payload sent to the accreditor, when recorded. |
owner_key | string | Key of the organization the record belongs to. |
regkey | object | Participant registration, including external (data from a connected external system). |
creditkey | object | The credit itself — name and cehours (contact hours). |
accreditationkey | object | Where the credit was reported, e.g. CE Broker or CPE Monitor. |
lectkey | object | Lecture 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:
- Poll every N minutes.
- Send
request_datetimeas the current time in ISO 8601 with aZor offset. - Send
look_back_minutesas roughly N + 50% (for example,look_back_minutes=20for a 15-minute poll). - Page through the result set with
end_cursoruntilhasmoreisfalse. - 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.