n8n-nodes-getcourse
v0.4.1
Published
GetCourse nodes for n8n: the Tech API v1, the legacy Import/Export API and a webhook trigger
Maintainers
Readme
n8n-nodes-getcourse
n8n community nodes for GetCourse — the Russian online-school platform — covering both of its APIs plus a webhook trigger.
GetCourse has two APIs that do not overlap and neither replaces the other:
| | GetCourse | GetCourse Legacy |
|---|---|---|
| API | Tech API, /pl/api/v1/… | Import/Export API |
| Docs | redoc | help/api |
| Keys | developer key + school key | the account's secret key |
| Creates | nothing | users, orders, payments |
| Bulk reads | offers, webinars, dictionaries — but never users or orders | users, orders, payments, group members |
| Single reads | users, orders, offers, dialogs, lessons, webinars | nothing |
So a real integration usually needs both: the Legacy node to create a user or export a month of orders, the GetCourse node to read one order or move it to another status, and the Trigger node to hear about it when something happens.
- Installation
- Credentials
- GetCourse node
- GetCourse Legacy node
- GetCourse Trigger
- Exporting data
- Limits
- Data quirks worth knowing
- Feedback and bugs
Installation
In n8n: Settings → Community nodes → Install, then enter
n8n-nodes-getcourse.
Self-hosted, from the command line:
npm install n8n-nodes-getcourseRequires n8n 2.x and Node 20.19 or newer.
Credentials
All three nodes share one credential, GetCourse API. There were two until 0.2.1 — see the changelog if you are upgrading.
Account address. Pick GetCourse Subdomain and enter the part in front of the domain —
myschool for myschool.getcourse.ru. Pasting the whole address, or the whole URL, works
too; everything from the first dot on is dropped and the Domain dropdown decides the
rest. If your account force-redirects that address to a domain of its own, pick Custom
Domain and enter that domain instead: the API answers only on the domain the account
actually serves, and a redirect is not followed, because following one would strip a POST
body and hand the key to the other host.
School API Key — the account's own key, one per school. GetCourse calls it «ключ АПИ школы» for the Tech API and shows the same value as the secret key under Профиль → Настройки аккаунта → АПИ, which is what the Import/Export API takes. Every node here uses it. A read-only key is enough for reading; importing through the Legacy node needs one with write access.
Developer Key — needed by the GetCourse node and the Trigger, and by nothing else.
Issued to the integrator after this form, and it is
the same key for every school you integrate. Leave it empty if you only import and export.
The Tech API is reached with Authorization: Bearer <developer key>_<school key>; a 403 means
one half is wrong, the two belong to different schools, or the school has not enabled that
developer key.
Test checks the half you filled in: with a developer key it asks the Tech API, without one it asks the Import/Export API.
Two things worth knowing before you debug an import
The Import/Export API is available on paid plans only, and a plan that does not include it
does not announce itself with error_code 917 the way the help page suggests. On such a
plan exports, the group list and the field dictionary can all work normally, while every
import answers HTTP 200 with an empty body and writes nothing — no code, no message. The
node reports that as «GetCourse answered nothing at all» rather than guessing. Check the
account's plan first and the key's write permission second. The Tech API is not subject to
this restriction.
Export Requests per Hour (default 45) is a throttle this node applies on your behalf. GetCourse allows an account 100 Export API requests per two hours in total, counting every status check, and going over answers 903 for every other integration on that account until the window clears. The default leaves room for a second workflow; lower it if the account has other integrations, raise it if this n8n is the only caller.
GetCourse node
The Tech API. Reads and updates existing objects; it creates nothing, and it cannot list users or orders at all.
| Resource | Operations | |---|---| | User (27) | Get · Get by Chat ID · Get by Telegram Chat ID · Get Custom Fields · Get Deals · Get Purchases · Get Trainings · Get Schedule · Get Goals · Get Diplomas · Get Groups · Get Balance · Get Lesson Answers · Get Survey Answers · Get Dialogs · Get HelpDesk Dialogs · Update · Update Custom Fields · Update Purchase · Add to Groups · Remove From Groups · Set Groups · Add Balance · Change Scale Points · Set Personal Manager · Create Diploma · Add Comment | | Order (11) | Get · Get Custom Fields · Get Comments · Get Calls · Get Many Tags · Get Cancel Reasons · Update · Update Custom Fields · Add Positions · Remove Positions · Add Comment | | Offer (3) | Get · Get Many · Get Many Tags | | School (9) | Get Many Groups · Get Many Departments · Get Many Trainings · Get Many Personal Managers · Get Survey Answers · Get Scale Results · Get Many Mailings · Get Mailing Stats · Get Mailing Content | | Dialog (5) | Add Comment · Add Note · Change Department · Close · Get History | | HelpDesk Ticket (5) | Add Comment · Add Note · Change Department · Close · Get History | | Lesson (3) | Get Many Answers · Add Comment to Answer · Set Answer Status | | Webinar (5) | Get Many · Get Many by IDs · Add Comment · Moderate Comment · Moderate User | | Call (2) | Set Description · Set Transcription | | Webhook (2) | Subscribe · Unsubscribe |
Dropdowns read the live account wherever the API publishes a list: user groups, departments, personal managers, offers, cancellation reasons and webinars. Custom fields have one too — see below for where it reads them from. Where neither API publishes a listing — lessons, surveys, products, diploma templates — the field is a plain input and says so.
There is no listing of users or of orders anywhere in this API, which is why the node has no "Get Many Users": a user is reached by ID, e-mail, phone or messenger chat ID, an order by its ID or through its buyer. Reading them in bulk is the Legacy node's export.
Two things the API does that are easy to be caught by, and which the node states in the operation description rather than leaving to discovery:
- Set Groups replaces the whole membership list. Add to Groups and Remove From Groups are the incremental pair.
- Call → Set Description and Set Transcription overwrite. A second call replaces the text rather than appending to it, so a loop keeps only the last value.
Naming a user
Most user operations take one of three identifiers, and the node asks which one you mean rather than letting you fill in two and leaving the server to choose:
- User ID — unambiguous, and the only form every endpoint accepts;
- Email — when the ID is unknown;
- Phone — documented for the write endpoints and for Get. The other reads may ignore it and answer as though no user was named.
Custom fields
The Tech API writes custom fields by numeric ID and has no method that lists the account's
fields, so the picker under Update Custom Fields gets them elsewhere. For user fields
it stays on the Tech API, where nothing is metered: it reads the personal-manager list, which
takes no parameters, and asks get-custom-fields about the first one — the answer describes
the account rather than that person. For order fields, and on an account with no personal
managers, it falls back to the account dictionary on the Import/Export API, which costs one
request from that budget and is remembered for two minutes; Refresh List, in the ⋮ menu beside the field, re-reads it once that window has passed. The IDs are the same numbers in
both APIs and they are stable.
Get Custom Fields is worth knowing about separately: it returns every field the account defines, not only the filled ones, with the name beside each value. The user and order versions answer different shapes — an object keyed by ID for a user, a plain array for an order — so the user operation emits one item carrying every field and the order operation one item per field.
Typing an ID by hand still works, and so does an expression.
Purchases and achievement scales
Update Purchase changes the start and finish dates, the period type and the responsible
teacher of a purchase. The purchase is named by its own ID — id in a row of User → Get
Purchases, not product_id — and nothing else, so no user needs to be named. GetCourse takes
the dates only as YYYY-MM-DD HH:MM:SS and refuses an ISO string or a bare date; the node
converts whatever the date picker or an expression gives, in the workflow's timezone. Switching
the period type between limited and unlimited leaves both dates as they were. A blank field
means "leave alone": to empty a date or remove the teacher, name it under Fields to Clear.
The answer is empty, so the node reports what it sent; read the purchase again to see it stored.
Get Scale Results on the School resource lists every user's points on every achievement
scale, one row per user and scale. No method names the scales or lists them, so a row carries
only scale_id. Change Scale Points on the User resource adds points to a scale or, with a
negative number, takes them away. It names the user by numeric ID only, as its specification
does. The specification says the answer is the history record the change made. A successful
change has not been tried on a live account yet, only the refusal of a scale that does not exist.
Mailings
Three reads on the School resource, all added to the Tech API in September 2026.
Get Many Mailings lists the school's mailings newest first: title, type, channel, category
and parent. Filters narrow it by category, a list of mailing IDs, a parent mailing, a part of
the title (GetCourse finds it anywhere in the title), the channel by name — email, sms,
vk, ticket — and the type: manual, notification, queue or template. Include Settings
adds each mailing's sending settings under params: recipients, sender, reply address,
schedule and design. Manual mailings pile up over the years, so a working school can hold a very
long list; filter it rather than reading everything with Return All.
Get Mailing Stats gives one row per mailing with its counters: recipients, sent, viewed,
clicked, answered, errors, unsubscribed, cancelled, restricted, queued, in progress and new. It
takes the same filters except the parent and the title. A row carries the mailing's id and no
title; join it to Get Many Mailings on id.
Get Mailing Content reads one mailing by ID: subject, content (HTML) and files.
GetCourse sends files as JSON written into a string; the node parses it, and keeps the string
as it came under files_json.
Both lists page by 1000, the most GetCourse gives at once, and neither reports a total. The list
is newest first and read by offset, so a mailing created while a long list is being read pushes
the rest down a row; the node drops the row that comes back twice. Several mailing IDs go out as
ids[]=…&ids[]=…, the way the specification writes them: a comma list or a repeated ids gets
a server error.
GetCourse Legacy node
The account Import/Export API.
| Resource | Operation | What it does | |---|---|---| | User | Import | Creates the person, or updates them when Update Existing is on. Groups given here are added to the ones they are already in. | | | Replace Groups | Sets the whole membership list — every group not named is removed. Needs the numeric user ID. | | Order | Import | Creates the order and the buyer with it. Repeating it with the same order number edits that order instead. | | | Set Status | Moves an existing order to another status. | | Export | Export and Wait | Starts the job, waits for GetCourse to build the file, returns the rows. | | | Start | Returns the export ID only. | | | Check Status | Whether the file is ready, its column list and its row count — without the rows. | | | Get Result | The rows of a finished export, or a report that it is still building. | | Group | Get Many | Every group in the account, with its ID and when someone was last added. Answers directly, with no export job. | | Custom Field | Get Many | The custom-field dictionary: ID, title, type and the entity each field belongs to. The only listing either API publishes. |
Importing an order
The minimum GetCourse accepts is the buyer's e-mail plus either an Offer ID or an offer code (or product title) together with an amount. Editing an existing order needs the order number as well. An order carries one offer per request: to add a second, import again with the same order number and Append Offers switched on.
An import is metered. Creating objects counts against a monthly allowance that depends on the account plan; updating them does not. GetCourse says plainly that this API is for importing data, not for reacting to things — for that, use the Trigger.
GetCourse Trigger
GetCourse has two unrelated webhook systems, and the trigger's Source picks between them.
Source: GetCourse Process
Works on any paid account and needs no developer key. Copy the node's webhook URL, then in
GetCourse create a process with the «Вызвать URL» operation pointing at it — method
POST, body json or x-www-form-urlencoded.
There is no fixed payload: the field names and the field set are whatever the person
configuring the process types in, built from placeholders like {object.email} in a user
process and {object.user.email} in an order process. The node therefore passes the body
through unchanged and validates nothing.
Source: Tech API Subscription
The node subscribes itself through POST /set-uri when the workflow is activated and
unsubscribes when it is deactivated. Pick one Event Object and the events under it.
One node listens to one event object on purpose. An Входящие message and a HelpDesk message
arrive as byte-for-byte identical bodies — same event_id, same dialog_id field name,
same everything — so the only thing that can tell them apart is which subscription
delivered them. Splitting by object keeps every delivery attributable, and the node fills
in an eventType accordingly.
The subscription belongs to the developer key, not to the workflow: deleting the node without deactivating it first leaves GetCourse posting into a dead URL.
Verifying deliveries
GetCourse signs nothing it sends, over either mechanism: if the address leaks, anyone can post to it.
For GetCourse Process there is something to be done about that. Under Verification the node can require a shared secret in a header or a query parameter, and answers 403 without starting the workflow when it is missing — add the same header in the «Вызвать URL» settings of the process.
For a Tech API Subscription there is not. GetCourse composes the delivery itself and sends a body and nothing else, so no secret of yours can be attached to it and the setting is not offered. The URL is the only thing keeping those events private.
Nor can a subscription be tested with n8n's Listen for test event: set-uri holds one
address per event, so subscribing the temporary test URL would replace the live one and
unsubscribing it afterwards would take the running workflow off the air. The node refuses
that rather than doing it. Activate the workflow to receive real events.
Exporting data
An export is asynchronous: one call starts the job and returns an ID, a second call either says "not ready" or hands over the whole table. The node offers that as one operation or as three, and both shapes are legitimate.
One node. Export → Export and Wait does the lot. Simple, and right for a nightly report — but it holds the execution open while it waits, and a restart of n8n loses the wait.
[GetCourse Legacy] Export → Export and Wait → one item per rowThree nodes. Start, then an n8n Wait node, then Get Result. The wait costs no execution time and survives a restart, which is what a large export needs. Set Get Result's On Not Ready to Return Status and an IF node can loop back:
[Start] → export_id
↓
[Wait 30s]
↓
[Get Result] On Not Ready: Return Status
↓
[IF ready == false] ──yes──→ back to [Wait 30s]
│ no
↓ one item per rowCheck Status exists for a chain that wants an explicit readiness step. It is honest about its cost: checking and fetching are the same request underneath, so a Check-then-Get chain spends one request more per loop than Get Result alone.
An export needs at least one filter — GetCourse refuses to build a file without one, and an account-wide export of a school with a long history can be very large. Split long periods into several runs.
Whenever Export and Wait gives up or fails part-way through — the file is still building, or the account's request budget ran out mid-poll — the error names the export ID. Nothing is lost: the job goes on building on GetCourse's side, and a Get Result with that ID collects it later, in this run or a later one. That matters because there is no method that lists exports, so an ID nobody wrote down cannot be recovered.
Dates and timezones
Filter dates are sent as whole days, worked out in the workflow's timezone — the one n8n shows in the workflow settings, not the account's. A school in a different zone can see the boundary fall a few hours out; set the workflow timezone to the school's to line them up.
A date written the Russian way, 01.03.2026, is read as the first of March. That has to be
said because the obvious implementation gets it wrong: JavaScript's own parser reads the same
string as the third of January, and only for the first twelve days of a month — from the
thirteenth it fails instead. Anything the node cannot read with certainty is refused by name
rather than guessed at, because an export whose filter is ignored returns the whole account.
The shape of the result
GetCourse answers with a table, not a list of objects: a column list and an array of arrays. The node zips them, one output item per row. The column set is account-specific — every custom field is spliced into the middle of the row — so nothing may be read by position.
Column titles are mostly Russian, with spaces and punctuation: $json["Создан"],
$json["ID партнера"]. A few arrive in Latin — utm_source and the five
gc_system_user_utm_* fields GetCourse adds to every account. Set Options → Column
Names → Transliterated to get sozdan instead, at the cost of exactness. An export whose
filter matches nothing is not an error: it returns the full column list and no rows, so the
node emits no items. Every cell arrives as a string, including money in Russian format
(12 000,00, sometimes with a non-breaking space) — the node does not coerce.
Limits
| | | |---|---| | Export API | 100 requests per two hours per account, status checks included. One export at a time per account. | | Import API | A monthly allowance on creating objects, set by the account plan. Updates do not count. | | Tech API | No published rate limit. The credential throttles to 5 requests per second by default as a precaution. |
The export budget is enforced by this package across the whole n8n process, per account host, so several workflows hitting one account add up to one budget rather than racing.
The one-export-at-a-time rule deserves planning for rather than reacting to. It is account-wide, so anything else exporting from the same school holds it — on an account where other integrations export too, a start refused with «Уже запущен один экспорт» can be routine rather than rare. Give the Export node a single item, or loop items through a Wait node, and turn on Settings → On Error → Continue so a refusal does not discard the export IDs earlier items already returned.
Data quirks worth knowing
Observed in the wild rather than documented. The nodes do not silently correct any of them.
- A set-uri delivery arrives wrapped:
{"json": { … }}. The trigger unwraps it, so$json.deal.statusis what you write, not$json.json.deal.status. deal.isPayedin a webhook is sometimes0/1and sometimes a boolean — and is0in adealPaidevent even for a priced order that was just marked paid, not only for a free one.tsis not the event's time. It is the order'screatedAt, and it is identical in every delivery about that order however far apart the events were. Do not deduplicate or order on it.tsis not UTC either, despite ending inZ. It is the account's local time with aZappended, so it is off from the real instant by the account's own UTC offset. Usets64when the real instant matters.deal.statustakes the literal string"false", which is GetCourse's «Ложный» status.deal.payedValuecan be fractional and less thancostwhile the status ispayed.answer.idarrives as a string with single quotes inside it:"'12345'".comment.filesis a string containing JSON and needs parsing.additional_fields[].requiredis sometimes a boolean and sometimes the string"1".- A paid order fires both
dealPaidanddealStatusChanged— confirmed on a live account, as two separate deliveries whose bodies differ ineventTypeand nothing else. Deduplicate if you act on both. - Deliveries carry no signature and no authentication header of any kind, and they arrive from more than one address, so neither a shared secret nor an IP allowlist is available. The secrecy of the URL is the only thing protecting a subscription.
- Export cells are always strings, dates as
YYYY-MM-DD HH:MM:SSin the account's timezone.
Compatibility
Built against n8n 2.x with @n8n/node-cli. No runtime dependencies.
Feedback and bugs
Bug reports and ideas are welcome — open an issue: github.com/zenland-dev/n8n-nodes-getcourse/issues.
What makes a report quick to act on:
- your n8n version and the version of this package;
- which node — GetCourse, GetCourse Legacy or the Trigger — and which operation;
- what GetCourse answered. It likes to reply
200 OKwithsuccess: false, so the body matters more than the status code; strip the keys before pasting it; - for an export, the export id and whether it ever left the
processingstate; - for the trigger, the source — a GetCourse process or a Tech API subscription — and one sample payload with the personal data removed.
GetCourse's own quirks are collected in Data quirks worth knowing; if you hit a new one, it belongs in an issue too — the list grows from them.
