CoreInto has a JSON API so other programs, such as your website, a control panel or a script, can read and change records: clients, orders, services, domains, invoices, payments, tickets and more. A program signs in with an API credential made under Settings > API Credentials, calls an action at /api/v1/ACTION, and gets a JSON answer. Every call is checked against the credential's scopes, is limited to the addresses and the expiry you set, and is written to the API log. This chapter covers the credentials, how to call the API, the answers and errors, and a reference of every action.
Settings > API Credentials
The list shows each credential's description, identifier, the administrator it acts as, its access (Everything, or how many areas), allowed addresses, when and from where it was last used, and when it expires. The API Reference button opens the in-app reference page, which repeats the table at the end of this chapter with your own API address filled in.
Add Credential asks for:
| Field | What it does |
|---|---|
| Description | What the credential is for, for example "website order form". |
| Acts as administrator | Changes made through the API are written to the activity log under this administrator's name ("API: username"), and disabling the administrator stops the credential. You can only choose an enabled administrator whose permissions you hold yourself (see Staff and security). The choice is fixed once the credential is made. |
| Allowed addresses | One IP address or range per line, for example 203.0.113.7 or 198.51.100.0/24. Empty means any address. |
| Expires on | A date after today. Empty means the credential never expires. On and after that date the credential stops working. |
| What it may reach | Tick Everything, or the scopes the credential needs (see below). At least one. |
When the credential is created, the page shows the identifier (32 characters) and the secret once. Copy the secret then: CoreInto keeps only its SHA-256 fingerprint and cannot show it again. If it is lost, open the credential and press Make New Secret, which gives a new secret and stops the old one. Delete removes the credential; programs using it can no longer connect.
Scopes
A scope is an area of the API. An action needs the scope shown in the reference; system.whoami needs none.
| Scope | Allows |
|---|---|
clients:read | Read clients, users and contacts |
clients:write | Add and change clients, users and contacts |
orders:read | Read orders |
orders:write | Place, accept and cancel orders |
services:read | Read services and add-ons (and licences) |
services:write | Change services and run module commands (and reissue licences) |
domains:read | Read domains |
domains:write | Change domains and run registrar commands |
billing:read | Read invoices, transactions, credit and quotes |
billing:write | Create invoices, add payments and credit |
support:read | Read tickets, departments and announcements |
support:write | Open and reply to tickets |
products:read | Read products, prices and promotions |
system:read | Read the system counts |
system:write | Send email and log activity |
Calling the API
Address
Calls go to your site's address followed by /api/v1/ and the action:
https://www.example.com/api/v1/clients.get
Short addresses need the .htaccess rewrite that comes with CoreInto (Apache and LiteSpeed). Where addresses cannot be rewritten, name the action as a parameter instead:
https://www.example.com/api/index.php?action=clients.get
The action field may also be sent in the request body.
Signing in
Send the identifier and secret in the Authorization header:
Authorization: Bearer IDENTIFIER:SECRET
If your web server drops the Authorization header, send them as the fields identifier and secret instead (in the JSON body, the form or the query string). They are removed from the parameters before the action runs and never written to the log.
A credential is accepted when the identifier and secret match, its administrator is enabled, it has not expired, and the caller's address is in its allowed list (or the list is empty). Each successful sign-in records the time and address as "last used".
Parameters
Parameters go in one of three places, in this order of preference:
- A JSON object in the request body, with
Content-Type: application/json. - Form fields (
application/x-www-form-urlencodedor multipart). - The query string. Query values sit beneath the body: a name given in both takes the body's value.
Conventions:
- Money is text with two decimals, such as
"1500.00". Dates areYYYY-MM-DD. - Yes/no values accept
1,true,yesoronfor yes; anything else is no. - Lists (such as
items,linesornameservers) are JSON arrays; in a form or query string they may be a JSON text or, for simple lists, comma-separated. - List actions take
limit(1 to 250, default 50) andoffset(default 0) and returntotal,offset,countand the rows. - Actions that change data must be called with POST; reading actions may use GET or POST.
- Ids are returned as numbers. Columns holding passwords, secrets, access keys and the like are never returned.
Answers
Every answer is JSON with a matching HTTP status. Success:
{"result": "success", "client": {"id": 1001, "first_name": "Sara", ...}}
Failure:
{"result": "error", "message": "There is no such client."}
| HTTP status | Meaning |
|---|---|
| 200 | Success. |
| 400 | Wrong or missing input; the message says which parameter. Also an action name missing or a body that is not valid JSON. |
| 401 | Not signed in: no credential given, or it was not accepted (wrong, expired, from an address it may not use, or its administrator is disabled). |
| 403 | The credential lacks the scope the action needs. |
| 404 | No such action, or no such record. |
| 405 | The action changes data and was not called with POST. |
| 409 | The request is understood but cannot be carried out now, for example accepting an order that is not pending, or a payment on a cancelled invoice. |
| 429 | Too many failed sign-ins from this address; wait 15 minutes. |
| 500 | Something went wrong in CoreInto; the details are in the error log. |
| 503 | CoreInto is not installed yet, or a database update is waiting for an administrator to apply it in the admin area. |
Rate limiting
After 10 failed sign-ins from one address within 15 minutes, that address is refused for 15 minutes with status 429. IPv6 callers are counted by their /64 network. A successful sign-in clears the count. There is no limit on successful calls.
The API log
Every call, successful or not, is written to Utilities > API Log with the credential, action, address, HTTP status, time taken, the parameters and the answer. Parameters whose names contain pass, secret, token, epp or auth are stored as "(hidden)". The log can be filtered by date, action, address and failed calls only, and old entries can be removed under System Cleanup. The setting "Log every API call" on the Security tab of General Settings has no effect in this version: calls are always logged.
Examples
Who am I, with GET:
curl https://www.example.com/api/v1/system.whoami \
-H "Authorization: Bearer 3f9c2b7e1a4d4c6f8e0b2a1d9c7e5f31:kq8Zr2vN0tXy5WbLm3Hc7dPa9Rs1Ue4Fg6"
A client by email address, with JSON parameters:
curl -X POST https://www.example.com/api/v1/clients.get \
-H "Authorization: Bearer IDENTIFIER:SECRET" \
-H "Content-Type: application/json" \
-d '{"email": "sara@example.com"}'
Place an order for one product and one domain, and email the confirmation:
curl -X POST https://www.example.com/api/v1/orders.add \
-H "Authorization: Bearer IDENTIFIER:SECRET" \
-H "Content-Type: application/json" \
-d '{"client_id": 1001, "email": true,
"items": [
{"type": "product", "product_id": 3, "term": "annually", "domain": "example.com"},
{"type": "domain", "name": "example.com", "action": "register", "years": 1}
]}'
Record a bank transfer against an invoice, as form fields:
curl -X POST https://www.example.com/api/v1/invoices.add_payment \
-H "Authorization: Bearer IDENTIFIER:SECRET" \
-d invoice_id=5012 -d amount=49.00 -d gateway=banktransfer -d transaction_id=TRF-20261009-17
Where addresses cannot be rewritten, and the Authorization header is dropped by the server:
curl "https://www.example.com/api/index.php?action=tickets.list&open_only=1" \
-d identifier=IDENTIFIER -d secret=SECRET
Reference
Parameters marked with * are required. "Changes data" means the action must be called with POST.
System
| Action | Scope | Parameters | Returns |
|---|---|---|---|
system.whoami | none | credential (id, description, scopes, expires_on), staff (the administrator's username), version, time. Useful to test a connection. | |
system.stats | system:read | stats: clients_active, orders_pending, services_active, domains_active, invoices_unpaid, invoices_overdue, tickets_awaiting_reply, cancellation_requests. | |
email.send (changes data) | system:write | client_id*; then either template (the name of an email template) with vars (an object of merge fields), or subject and message (merge fields such as {{client_first_name}} work) | email_id. 409 when the email was not sent: the template is turned off, the client turned that kind of email off, or the address is not valid. |
activity.add (changes data) | system:write | description*, client_id | Nothing more; the line is in the activity log under the credential's administrator. |
Clients
| Action | Scope | Parameters | Returns |
|---|---|---|---|
clients.list | clients:read | search (name, company or email contains), email (exact), status (active, inactive, closed), limit, offset | clients, newest first: id, first_name, last_name, company, email, phone, country, status, currency_id, group_id, credit, created_at. |
clients.get | clients:read | id, or email | client: every profile field, plus currency (code), users (id, email, names, is_owner, permissions), contacts, and counts of what the account holds. |
clients.add (changes data) | clients:write | first_name*, last_name*, email*, country*, company, address1, address2, city, state, postcode, phone, tax_id, currency_id (else chosen by country), group_id, password (else a random one), marketing, send_welcome, allow_duplicate_email | client_id, user_id, existing_user (true when a user with that email existed and became the owner). 409 when a client with that email exists and allow_duplicate_email is not set. |
clients.update (changes data) | clients:write | id*; any profile field; status (active, inactive, closed); group_id (0 for none); marketing; the flags tax_exempt, late_fee_exempt, overdue_notices_off, separate_invoices, auto_capture_off, auto_close_exempt, allow_sso | changed: the fields that changed. Only the fields sent change. 409 when another client has the new email address. |
clients.close (changes data) | clients:write | id* | Nothing more; the account is closed. |
clients.credit (changes data) | billing:write | client_id*, amount* (negative to take credit away), description* | credit: the new balance. 409 when taking away more than the client has. |
contacts.list | clients:read | client_id* | contacts. |
Catalogue
| Action | Scope | Parameters | Returns |
|---|---|---|---|
products.list | products:read | group_id, include_hidden (also hidden and retired products) | products: id, group_id, group_name, type, name, slug, short_description, pay_type, hidden, retired, module, stock_control, stock_qty, and prices as currency code, then term, then setup and price. |
tlds.list | products:read | tlds: id, tld, registrar, terms, auto_register, docs_required, epp_required, and register, transfer and renew price tables by currency and term (y1 to y10). | |
promotions.list | products:read | promotions: id, code, type, value, recurring, starts_on, expires_on, max_uses, uses, notes. |
Orders
| Action | Scope | Parameters | Returns |
|---|---|---|---|
orders.list | orders:read | client_id, status, limit, offset | orders, newest first: id, number, client_id, status, amount, currency, gateway, invoice_id, promo_code, source, created_at. |
orders.get | orders:read | id* | order with its items. |
orders.add (changes data) | orders:write | client_id*, items* (see below), promo (a promotion code), gateway (a payment method name), notes, invoice (default true: make the invoice), email (send the order confirmation), accept (accept the order at once), setup (with accept: set products up on their servers now) | order_id, order_number, invoice_id (null when nothing is billed), service_ids, domain_ids. Prices come from the catalogue; nothing the caller sends is used as a price. 400 with the reason when an item cannot be ordered. |
orders.accept (changes data) | orders:write | id*, setup (default true), email (default true: welcome emails) | welcome_emails: how many were sent. 409 unless the order is Pending. |
orders.cancel (changes data) | orders:write | id*, fraud (mark as fraud instead) | Nothing more; pending items and the unpaid invoice are cancelled. 409 when already cancelled. |
Items for orders.add are objects in a list. A product:
{"type": "product", "product_id": 1, "term": "monthly", "qty": 1, "domain": "example.com",
"options": {"4": 2}, "addons": [3], "ns1": "", "ns2": ""}
options maps a configurable option id to the chosen value id (or quantity); addons lists add-on ids. A domain:
{"type": "domain", "name": "example.com", "action": "register", "years": 1, "epp": "", "nameservers": []}
action is register or transfer; epp is the transfer code.
Services
| Action | Scope | Parameters | Returns |
|---|---|---|---|
services.list | services:read | client_id, product_id, server_id, status, domain, limit, offset | services, newest first: id, client_id, order_id, product_id, product, server_id, status, domain, username, billing_cycle, first_amount, recurring_amount, reg_date, next_due_date. |
services.get | services:read | id* | service with options (option_id, name, value, qty) and addons (id, addon_id, name, status, billing_cycle, recurring_amount, next_due_date). |
services.update (changes data) | services:write | id*; domain, username, status (pending, active, suspended, terminated, cancelled, fraud, completed), billing_cycle, first_amount, recurring_amount, next_due_date, next_invoice_date, reg_date, termination_date, server_id (0 for none), dedicated_ip, assigned_ips, suspend_reason, notes | changed. Changes the record only, not the account on the server. |
services.command (changes data) | services:write | id*, command* (create, suspend, unsuspend, terminate, change_package, change_password), reason (for suspend), password* (for change_password), email (default true: send the matching client email) | message from the module. 409 with the module's message when it fails. |
Licences
These belong to the Licensing Manager add-on; see Licensing Manager.
| Action | Scope | Parameters | Returns |
|---|---|---|---|
licenses.list | services:read | client_id, service_id, status (active, reissued, suspended, expired), limit, offset | licenses, newest first: id, service_id, client_id, license_key, status, allowed_ips, allowed_hosts, allowed_dirs (lists), max_ips, reissues, last_check_at, last_ip, last_version, created_at. |
licenses.get | services:read | id, key or service_id | license with its last 20 checks (created_at, result, ip, host, dir, version). |
licenses.reissue (changes data) | services:write | id, key or service_id; count_for_client (count it towards the client's own reissue limit) | license after the reissue: its addresses, domains and folders are cleared and the next check binds it again. |
Domains
| Action | Scope | Parameters | Returns |
|---|---|---|---|
domains.list | domains:read | client_id, status, registrar, name, expires_before (a date), limit, offset | domains, newest first: id, client_id, order_id, name, type, registrar, status, period_years, first_amount, recurring_amount, reg_date, expiry_date, next_due_date, auto_renew, nameservers. |
domains.get | domains:read | id, or name | domain. |
domains.check | domains:read | name* (a full domain name) | name, available (true, false, or null when it cannot be told), sold_here (whether you sell the extension). |
domains.update (changes data) | domains:write | id*; status, registrar, period_years (1 to 10), first_amount, recurring_amount, reg_date, expiry_date, next_due_date, next_invoice_date, auto_renew, dns_mgmt, email_fwd, id_protect, notes | changed. Changes the record only, not the domain at the registrar. |
domains.command (changes data) | domains:write | id*, command* (register, transfer, renew, sync, nameservers, lock, unlock, send_epp), years (for renew, 1 to 10), nameservers (a list, for nameservers) | message from the registrar. 409 with the registrar's message when it fails. send_epp asks the registrar for the transfer code and emails it to the client (or passes the request to staff when the extension is handled by hand). |
Billing
| Action | Scope | Parameters | Returns |
|---|---|---|---|
invoices.list | billing:read | client_id, status, issued_from, issued_to, due_before, limit, offset | invoices, newest first: id, number, client_id, status, issued_on, due_on, paid_at, currency, subtotal, tax1, tax2, credit, total, gateway. |
invoices.get | billing:read | id* | invoice with currency, balance, lines (id, kind, rel_id, description, amount, taxed, due_on) and transactions (id, paid_at, gateway, trans_id, amount_in, fees, amount_out, description). |
invoices.create (changes data) | billing:write | client_id*, lines* (a list of {"description": "...", "amount": "10.00", "taxed": true}), due_on (default today plus "Days to pay a new order"), gateway, notes, draft (make a draft instead of an unpaid invoice), apply_credit (use the client's credit on it), send (email it, unless a draft) | invoice_id, number, total, status. |
invoices.update (changes data) | billing:write | id*, due_on, notes, status (unpaid, cancelled, collections, payment_pending) | changed. Payments mark invoices paid; this action cannot. 409 when the invoice is paid. |
invoices.add_payment (changes data) | billing:write | invoice_id*, amount* (more than zero), gateway* (a payment method name), transaction_id, date (default now), fees, send_receipt (default true) | transaction_id (CoreInto's), status (the invoice's status after the payment), balance, to_credit (any overpayment added to the client's credit). A fully paid invoice sets its services and domains in motion like any payment. 409 when the invoice is a draft, cancelled or refunded, or when the same gateway transaction id was recorded before. |
invoices.send (changes data) | billing:write | id*, template (an invoice email, default invoice_created) | email_id. 409 when the email was not sent. |
transactions.list | billing:read | client_id, invoice_id, gateway, transaction_id, from, to (dates), limit, offset | transactions, newest first: id, client_id, invoice_id, gateway, trans_id, paid_at, currency, amount_in, fees, amount_out, description, refund_of. |
quotes.list | billing:read | client_id, limit, offset | quotes, newest first. |
quotes.get | billing:read | id* | quote with its lines. |
Support
| Action | Scope | Parameters | Returns |
|---|---|---|---|
tickets.list | support:read | client_id, department_id, status, email, open_only (statuses that count as open), limit, offset | tickets, latest reply first: id, mask (the ticket number), department_id, department, client_id, name, email, subject, status, priority, staff_id, last_reply_at, created_at. Merged tickets are left out. |
tickets.get | support:read | id, or mask; include_notes | ticket with messages (id, user_id, contact_id, staff_id, name, email, body, format, rating, created_at) and, with include_notes, the staff notes. |
tickets.open (changes data) | support:write | department_id*, subject*, message*; client_id, or name and email (a guest ticket); priority (low, medium, high), format (markdown or plain), as_staff (the credential's administrator writes it; the ticket starts Answered), notify (with as_staff: email the ticket, default true) | ticket_id, mask. Without as_staff the ticket is treated as opened by the client or guest: it starts Open, the automatic reply goes to them and staff are told. |
tickets.reply (changes data) | support:write | id or mask, message*, status (the status to set afterwards), format, as_client (reply as the ticket's client or guest instead of staff), notify (default true) | message_id. |
tickets.update (changes data) | support:write | id or mask; status, priority, subject, department_id, staff_id (0 for nobody), flagged | changed. |
departments.list | support:read | departments: id, name, description, email, hidden, clients_only, sort. | |
announcements.list | support:read | limit, offset | announcements, newest first: id, title, body (Markdown), published_at. Only published ones. |
