Audit log
Every administrative change to your account is recorded with who made it, what they changed, when, and from which IP address. Admins see it in Console → Audit log; the API serves the same entries.
What is recorded
| Action | When |
|---|---|
api_key.created, api_key.rolled, api_key.revoked | Key changes in the console (a key created by textkit login carries via: cli_login) |
api_key.viewed | An admin viewed or copied a key in the console (the key itself is never logged) |
api_key.paused, api_key.resumed | Our key monitor paused a live key (a sudden spike from a new country: requests today and the countries), or an admin resumed it |
cli_login.approved, cli_login.denied | Someone approved or denied a CLI login request (with the machine name the CLI sent) |
team.invite_created, team.invite_accepted, team.invite_revoked | Invitations |
team.role_changed, team.member_removed, team.member_left | Membership changes |
webhook.created, webhook.updated, webhook.deleted | Webhook endpoints (console or API) |
webhook.secret_rotated | A webhook endpoint's signing secret was replaced (the secret itself is never logged) |
webhook.secret_viewed | Someone revealed a webhook endpoint's signing secret in the console (the secret itself is never logged) |
spend_limit.updated | Spend limit changes |
credits.topped_up | Credits were bought (by a person or by auto-recharge) |
credits.settings_updated | Auto-recharge or the low-balance alert changed, including auto-recharge switching itself off after a declined card |
number.purchased, number.released, number.assigned | Numbers (bought, released, or assigned to a customer) |
consent.updated, consent.imported | Manual opt-out / opt-in overrides and imports |
migration.opt_outs_attested | An admin confirmed there are no opt-outs to carry over from a previous provider |
quiet_hours.updated | The account's quiet-hours default changed |
reconfirmation.updated | The account's re-confirmation setting changed; turning it off records the acknowledgment |
contact.topic_updated | A contact's subscription to a topic changed |
link_domain.created, link_domain.deleted, link_domain.activated, link_domain.deactivated | Link domains added or deleted, and turned on or off (on by itself once its DNS checks out) |
contacts.imported | A contact import finished (row counts, never the rows) |
segment.created, segment.updated, segment.deleted | Audience segments |
topic.created, topic.updated, topic.deleted | Subscription topics |
settings.updated | Account name, auto-replies, verified recipients, live-access requests |
number.registration_set, number.sender_linked, number.sender_unlinked | A number was attached to a registration, linked with the carriers, or unlinked (on release, when moved, or when our team undid an approval) |
number.attached | Our team added a number we ordered for you (a toll-free number, say) to your account, with or without the monthly number fee |
number.toll_free_updated | Our team recorded a toll-free verification result for a number: status, business and use case |
registration.approved, live_access.approved | Our team approved a registration, or live access for the account |
live_access.denied | Our team denied a live-access request, with the reason. The account stays in the sandbox and can ask again |
registration.approval_undone | Our team undid an approval it recorded by mistake: the registration went back to the filing queue or was returned to you with a reason, its numbers stopped texting the public, and the registration fees that approval started were waived. Live access is unchanged |
registration_fees.invoiced | Our team billed carrier registration fees: the invoice id, the amount and whether the saved card was charged |
business.attested, business.verified, business.rejected | Someone signed the business attestation (who, and for which registration), or our team verified or could not verify the business (with the reason code). Never the full EIN |
opt_in_page.enabled, opt_in_page.disabled | A hosted opt-in page for a registration was turned on or off |
registration.marketing_paused, registration.marketing_resumed | Marketing on a registration was paused for a high opt-out rate, or our team resumed it (with the reason) |
registration.sending_paused, registration.sending_resumed | Our team paused every message from a registration's numbers for review, or resumed it (with the reason) |
registration.content_approved, registration.content_approval_revoked | Our team approved a restricted content category for a registration, or revoked it (category, with the reason) |
registration.link_domain_added, registration.link_domain_removed | Our team allowed a domain in a registration's links, or removed it (domain, with the reason) |
template.approved, template.rejected | Our team reviewed a new kind of message for a registration (fingerprint, the reason, how many held messages were sent or dropped) |
template_review.updated | Our team changed whether the account's new kinds of message are reviewed (setting: auto for the first 30 days of live sending, always or off, with the reason) |
account.suspended, account.unsuspended | Our team suspended the account, or lifted the suspension (with the reason) |
account.sending_paused, account.sending_resumed | All live sending was paused (automatically, after messages blocked as phishing, impersonation or fraud, or by our team), or the pause was lifted (with the reason) |
account.strikes_reset | Our team cleared the account's strikes (with the reason) |
account.security_code_sent | TextKit texted a sign-in or phone-verification code for the account (the phone's last four digits) |
registration.ladder_raised, account.ladder_raised | The enforcement ladder moved a registration (or the account) to a warning, half pace, or paused, with the reasons |
registration.ladder_lowered, account.ladder_lowered | Our team moved it back down (with the reason) |
account.appealed | Someone on your team appealed a restriction from the console |
automation.created, automation.updated, automation.activated, automation.paused, automation.deleted | Automations (name, trigger event and step count; never message text) |
automation.identity_unlinked | A user_id to phone link was removed with DELETE /v1/automations/identities/{user_id} (the user_id; never the number) |
privacy.phone_erased | TextKit staff deleted one phone number's data from your account on a privacy request (the number's last four digits, what was removed as counts, and the reason). The number's opt-out is kept |
message.deleted, message.redacted | A message was deleted or redacted through the API (its direction and status, the other number's last four digits, and how many events and request logs were changed; never the text) |
privacy.email_erased | TextKit staff deleted one email address's data (its Verify email codes and the events about them) from your account on a privacy request (the address's domain only, what was removed as counts, and the reason) |
schedule.created, schedule.updated, schedule.paused, schedule.resumed, schedule.deleted | Recurring schedules (frequency, time, time zone and which fields changed; never the message text or the recipient) |
account.closed, account.reopened | The account was closed, by its owner or by our team (with what closing stopped and when the data is deleted), or our team reopened it before then. The log is readable in the console until the account's data is deleted, and is deleted with it |
Entries never contain message content, secrets, or full invite links.
GET /v1/audit-logs
curl "https://api.jointextkit.com/v1/audit-logs?limit=25" -H "Authorization: Bearer ssms_sk_live_..."{
"data": [
{
"id": "aud_a1B2c3D4e5F6g7H8",
"object": "audit_log",
"action": "api_key.created",
"actor": { "type": "user", "id": "uid_...", "email": "dev@example.com", "name": "Dev" },
"target": { "type": "api_key", "id": "key_x9Y8z7W6v5U4" },
"metadata": { "mode": "live", "name": "Billing worker", "scopes": ["messages:send"] },
"ip": "203.0.113.7",
"created_at": "2026-10-01T17:02:11.000Z"
}
],
"has_more": true,
"next_cursor": "..."
}Newest first. Pass next_cursor back as cursor for the next page;
limit is 1 to 100 (default 25). actor.type is user (console) or
api_key (with the key's id and name). Needs the audit_logs:read scope
on a restricted key.
Filters
| Parameter | Matches |
|---|---|
action | One action (number.released), or a whole group with a trailing dot (team., api_key.) |
actor | The actor's id (exact), or any part of their email, case-insensitive |
target_type | The target's type: api_key, member, invite, webhook, number, phone, ... |
created_after | Entries at or after this time (ISO 8601) |
created_before | Entries at or before this time (ISO 8601); a bare date means the end of that day, UTC |
curl "https://api.jointextkit.com/v1/audit-logs?action=team.&created_after=2026-09-01&created_before=2026-09-30" \
-H "Authorization: Bearer ssms_sk_live_..."Filters apply before paging, so a page holds limit matching entries. One
request scans at most 5,000 entries: on a long log with a rare filter, a page
can come back short (or empty) with has_more: true. Keep following
next_cursor until has_more is false. A date range narrows the scan
itself, so it is the fastest filter. An invalid filter returns
400 invalid_request.
Export as CSV
GET /v1/exports/audit_log streams the same entries as CSV and takes the
same filters. Columns: id, created_at, action, actor_type,
actor_id, actor_email, actor_name, target_type, target_id,
ip, metadata (JSON). Up to 50,000 rows per export; same
audit_logs:read scope. In the console, Export CSV on the Audit log
page downloads the current filter (admins only).
curl "https://api.jointextkit.com/v1/exports/audit_log?created_after=2026-01-01" \
-H "Authorization: Bearer ssms_sk_live_..." -o audit-log.csv