Migrating from 2.x
Version 3.0.0 is a clean break, and the version lives in the URL — so nothing you have today changes until you point a call at the new path. You can move one endpoint at a time.
1. Change the URL
- https://apigw.goninjio.com/api/api_gateway/customer/employees/
+ https://apigw.goninjio.com/api/api_gateway/3.0.0/customer/employees/
Your API key and the apiKey header are unchanged, and so are the scopes each endpoint
requires.
2. Read rows from data
This is the change that touches the most code, and it is mechanical. In 2.x, rows came back under eight different keys — or under none at all.
| Endpoint | 2.x rows were at | 3.0.0 |
|---|---|---|
/customer/employees/ | employees | data |
/customer/employee/deletion_logs/ | logs | data |
/customer/users/ | users | data |
/customer/groups/ | (bare array) | data |
/customer/groups/tags/ | (bare array) | data |
/customer/groups/dynamic/phishing/ | data | data |
/customer/groups/dynamic/training/ | data | data |
/customer/campaigns/phishing/ | (bare array) | data |
/customer/campaigns/training/ | (bare array) | data |
/customer/simulations/phishing/ | simulations | data |
/customer/simulations/training/ | trainings | data |
/customer/templates/phishing/ | templates | data |
/customer/templates/email/ | templates | data |
/customer/templates/training/ | templates | data |
/customer/templates/training/performance/{id}/ | data.employees_data | data |
/customer/simulations/training/{id}/performance/ | data.employees_data | data |
/customer/bundles/ | (bare array) | data |
/customer/domains/ | (bare array) | data |
/tenant/bundles/ | (bare array) | data |
/tenant/direct_customers/ | (bare array) | data |
/tenant/hierarchy/ | (bare array) | data |
/tenant/collections/ | data | data |
/tenant/mass_campaign_delivery/ | data | data |
Nine endpoints used to return a JSON array at the top level and now return an object.
If you did for row in response.json(), that becomes for row in response.json()["data"].
3. Add a pagination loop where you relied on getting everything
Those same nine endpoints were also unbounded — they returned every row, every time.
From 3.0.0 they return a page, so anywhere you assumed one response contained the whole
set, follow next_cursor. See Pagination.
4. Rename the page-size parameter
2.x spelled this four different ways depending on the endpoint. 3.0.0 accepts one:
| 2.x | 3.0.0 |
|---|---|
page_size | page_size |
per_page (deletion logs) | page_size |
items_per_page | page_size |
amount (mass delivery) | page_size |
5. Move off the duplicate envelope keys
2.x emitted the same two numbers under four extra names. They are gone:
| Removed | Use instead |
|---|---|
per_page | page_size |
total | total_items |
total_pages | num_pages |
current_page | page |
6. Expect 404 where you got 400
In 2.x a resource that did not exist could answer 400, 404, a Django HTML error page,
or even 200 with an empty object, depending on which endpoint you asked. In 3.0.0 it is
always 404 with a JSON body.
Notably, these used to answer 400:
- a campaign, simulation or dynamic group id that does not exist (or belongs to another account)
- an employee that does not exist
/customer/learner-report/when the account has no report
And /customer/templates/phishing/{id}/ used to return an HTML 404 page from inside a
JSON API. It now returns {"error": "..."} like everything else.
7. Error bodies are one shape
2.x used eight between them — error, errors, message, details, validation_error,
a raw list of validation objects, and plain text. 3.0.0 uses exactly one:
{ "error": "'page_size' must not exceed 1000" }
Other differences worth knowing
Timestamps are 2026-01-15T10:30:00+00:00 everywhere. 2.x used three different
formats, one of which (/tenant/collections/) dropped the seconds and the timezone
entirely, so those values gain precision.
Some listings carry more fields than their 2.x counterpart. The template catalogs
now always include level, quiz_average and quiz_events_count; dynamic group details
include type and is_training. These are additions — nothing was removed.
/customer/simulations/phishing/ and /customer/simulations/training/ defaulted to
20 rows per page in 2.x. They default to 100 now, like every other listing.
A page past the last one now returns an empty page, reporting the page number you
asked for. 2.x was inconsistent here: some endpoints raised a 500 (the training template
performance endpoint among them), others quietly answered with the last page, so a client
that overran received rows it had not asked for and had no way to tell.
Filtering by a date covers the whole of the day named. In 2.x a bare
created_date[lte]=2026-03-01 was read as midnight, so nearly all of 1 March fell outside
the range. Expect row counts to differ when you compare the two versions on a date filter;
3.0.0 is the one that matches what the parameter says. The filters_applied echo reports
the exact instant compared against.
Two endpoints that error on 2.x answer normally on 3.0.0. Fetching a single training
campaign raises a 500 on 2.x whenever its simulations carry an emotion, and the phishing
campaign listing raises a 500 if any campaign has no start or end date. Both are served
correctly here. 2.x is left as it is, because changing those responses would break callers
that have already worked around them.
Deleted records are no longer listed — on either version. A simulation, campaign or
mass delivery the customer deleted used to appear in listings even though the web UI had
stopped showing it, which put test data into reports and skewed statistics. Both 2.x and
3.0.0 now leave them out, and both accept ?include_deleted=true when you want the full
history. Fetching one by id is unchanged and still returns the record whatever its
state — but the lists inside a detail response are not: a campaign's simulations_ids
and emotions, and a template's template_simulations, all leave deleted simulations
out now, on both versions and with no flag. See
Filtering. If your integration filtered these out
client-side, that code can go.
/customer/templates/phishing/search/ is available on both versions. It existed on
2.x but was never registered with the gateway's authorisation layer, so every call to it
was refused with 403. If you ruled it out for that reason, it works now.
Not yet available in 3.0.0
These still exist on their 2.x paths and are unchanged there. Keep calling them at
/api/api_gateway/... until they land:
| Endpoint | Methods |
|---|---|
/customer/employees/ | POST |
/customer/employees/{employee_id}/ | PUT, DELETE |
/customer/employees/{employee_id}/training/performance/ | GET |
/customer/groups/ | POST |
/customer/groups/{group_id}/ | PUT |
/customer/campaigns/phishing/ | POST |
/customer/campaigns/training/ | POST |
/customer/campaigns/training/{campaign_id}/status/ | PUT |
/customer/simulations/training/ | POST |
/customer/templates/training/statistics/{template_id}/ | GET |
/customer/users/ | POST |
/customer/users/{user_id}/ | DELETE |
/customer/logo/ | POST |
/customer/learner-report/ | PUT, DELETE |
/customer/billing_report/download_csv/ | GET |
/tenant/billing_report/download_csv/ | GET |
/tenant/create/ | POST |
/tenant/customer/create/ | POST |
Mixing versions is fine: read through 3.0.0 and write through 2.x until the write endpoints are published.
Checklist
- Point one endpoint at
/3.0.0/in a test environment. - Change your row accessor to
data. - Add a pagination loop wherever you relied on receiving everything.
- Rename
per_page/items_per_page/amounttopage_size. - Replace
total,total_pages,current_page,per_pagewith the canonical names. - Treat
404as "not found" where you previously special-cased400. - Use
filters_appliedin your tests to confirm the server read your filters the way you intended.