ax-check.com
by Gauge
A
ActivityInfo
activityinfo.org
ax-check.com
Checked 19 September 2026
D
38/100
Agents need your login
Pricing is clear, but agent-facing docs are basically missing.
No documentation index or Markdown docs were found for agents, while pricing (Solo €545/yr, Programme €3,700/yr) is stated openly on the site.
3 fixes to start with
Concrete changes, backed by what we found.
/llms.txt returns HTTP 404, so no documentation index is published.
Linked from www.activityinfo.org/llms.txt
Recorded coding sessions
Same task, 3 models, no activityinfo.org account. Recorded 19 September 2026 on Pi.
3 recorded · 7m 49s
7m 49s · recorded
Session findings
Select a finding to see it in context.
›Thinking…
The user wants me to build a simple example using ActivityInfo. They want to know how pricing works, and whether the product will be easy for me to manage. I should check if there's an SDK or API. Let me investigate.
ActivityInfo is a SaaS data collection platform. It has an API and reportedly a Java SDK. Let me check what's available. I don't have internet access necessarily... Actually I might via bash? Let me check the environment.
Let me first look around the repo and check for any existing files, and check network access.
×$ ls -la && echo "---" && cat /etc/os-release 2>/dev/null | head -5 && echo "---" && which python3 node npm java curl 2>/dev/null✓└{"content":[{"text":"total 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Sep 19 06:13 ..\ndrwxr-xr-x 8 root root 4096 Sep 18 08:00…ctrl+o
{"content":[{"text":"total 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Sep 19 06:13 ..\ndrwxr-xr-x 8 root root 4096 Sep 18 08:00 .git\n-rw-r--r-- 1 root root 408 Sep 18 08:00 .gitignore\n---\nPRETTY_NAME=\"Ubuntu 24.04.4 LTS\"\nNAME=\"Ubuntu\"\nVERSION_ID=\"24.04\"\nVERSION=\"24.04.4 LTS (Noble Numbat)\"\nVERSION_CODENAME=noble\n---\n/usr/local/bin/python3\n/usr/local/bin/node\n/usr/local/bin/npm\n/usr/bin/curl\n\n\nCommand exited with code 1","type":"text"}],"details":{}}✓$ curl -sS -m 10 -o /dev/null -w "%{http_code}\n" https://www.activityinfo.org/ 2>&1 || echo "no internet"✓└{"content":[{"text":"200\n","type":"text"}]}
›Thinking…
Internet works. ActivityInfo is hosted SaaS. Let me look at their developer docs and pricing.
Let me check ActivityInfo's API docs and SDK. They have a Java SDK and a REST API. Let me explore.
✓$ curl -sS -m 15 https://www.activityinfo.org/about/developers/ 2>&1 | sed 's/<[^>]*>//g' | grep -iE 'api|sdk|java|python|rest|token|auth' | head -40✓└{"content":[{"text":" API guide\n","type":"text"}]}
✓$ curl -sS -m 15 https://www.activityinfo.org/about/pricing/ 2>&1 | sed 's/<[^>]*>//g' | grep -iE 'free|price|plan|per|user|month|project|rate|limit' | head -60✓└{"content":[{"text":"(no output)","type":"text"}]}
›Thinking…
Let me look deeper. Let me get the API guide URL.
✓$ curl -sS -m 15 https://www.activityinfo.org/about/developers/ -o /tmp/ai-dev.html; grep -oiE 'href="[^"]*"' /tmp/ai-dev.html | grep -iE 'api|sdk|java|python|dev|rest' | head -40✓└{"content":[{"text":"href=\"/apidocs/index.html\"\n","type":"text"}]}
✓$ curl -sS -m 15 https://www.activityinfo.org/about/pricing/ -o /tmp/ai-pricing.html; ls -la /tmp/ai-pricing.html; grep -oiE '<(h1|h2|h3)[^>]*>[^<]*' /tmp/ai-pricing.html | head -40✓└{"content":[{"text":"-rw------- 1 root root 12761 Sep 19 06:13 /tmp/ai-pricing.html\n<h1>Page not found\n","type":"text"}]}
›Thinking…
The pricing page may have moved. Let me find pricing from the main site. And get the apidocs.
✓$ curl -sS -m 15 https://www.activityinfo.org/apidocs/index.html -o /tmp/ai-api.html; ls -la /tmp/ai-api.html; sed 's/<[^>]*>//g' /tmp/ai-api.html | grep -vE '^\s*$' | head -60✓└{"content":[{"text":"-rw------- 1 root root 0 Sep 19 06:13 /tmp/ai-api.html\n","type":"text"}]}
✓$ curl -sS -m 15 https://www.activityinfo.org/ -o /tmp/ai-home.html; grep -oiE 'href="[^"]*"[^>]*>[^<]*' /tmp/ai-home.html | grep -iE 'pricing|price|plan|signup|sign' | head; echo "---search pricing---"…✓└{"content":[{"text":"href=\"/about/feature/form-designer.html\">\nhref=\"/about/pricing/index.html\">\nhref=\"/about/feature/form-designer.html\">\nhref=\"/abou…ctrl+o
curl -sS -m 15 https://www.activityinfo.org/ -o /tmp/ai-home.html; grep -oiE 'href="[^"]*"[^>]*>[^<]*' /tmp/ai-home.html | grep -iE 'pricing|price|plan|signup|sign' | head; echo "---search pricing---"; grep -oiE 'href="[^"]*(pricing|price)[^"]*"' /tmp/ai-home.html | head
{"content":[{"text":"href=\"/about/feature/form-designer.html\">\nhref=\"/about/pricing/index.html\">\nhref=\"/about/feature/form-designer.html\">\nhref=\"/about/feature/report-designer.html\">\nhref=\"/about/pricing/index.html\">\nhref=\"/about/pricing/onboarding.html\">\nhref=\"/about/pricing/partnerships.html\">\nhref=\"/about/feature/form-designer.html\">View all the features\nhref=\"/signUp\">Start a free trial\nhref=\"/about/pricing/partnerships.html\">Partnerships\n---search pricing---\nhref=\"/about/pricing/index.html\"\nhref=\"/about/pricing/index.html\"\nhref=\"/about/pricing/onboarding.html\"\nhref=\"/about/pricing/partnerships.html\"\nhref=\"/about/pricing/partnerships.html\"\n","type":"text"}]}✓$ curl -sS -m 15 https://www.activityinfo.org/about/pricing/index.html -o /tmp/ai-pricing.html; ls -la /tmp/ai-pricing.html; sed 's/<[^>]*>//g' /tmp/ai-pricing.html | grep -vE '^\s*$' | head -120✓└{"content":[{"text":"-rw------- 1 root root 28731 Sep 19 06:13 /tmp/ai-pricing.html\n Pricing\n ActivityInfo Logo \n Features\n Pricing\n C…ctrl+o
{"content":[{"text":"-rw------- 1 root root 28731 Sep 19 06:13 /tmp/ai-pricing.html\n Pricing\n ActivityInfo Logo \n Features\n Pricing\n Customers\n Support\n News\n Contact us\n Log in\n Menu Log in\n Subscriptions\n Onboarding\n Partnerships\n Features\n Form designer\n Report designer\n Integrations\n Data entry\n All features\n New features\n Pricing\n Subscriptions\n Onboarding\n Partnerships\n Customers\n Support\n Documentation\n Templates\n Training\n Webinars\n Onboarding\n News Contact us\n var menuButton = document.getElementById('menu-button');\n var menu = document.getElementById('menu');\n var menuToggled = false;\n menuButton.addEventListener('click', function(e) {\n if(menuToggled) {\n menu.style.display = 'none';\n menuButton.innerText = 'Menu';\n } else {\n menu.style.display = 'block';\n menuButton.innerText = 'Close';\n }\n menuToggled = !menuToggled;\n });\n Pricing\n We offer a range of subscription plans to suit everyone from small teams to large organizations.\n Solo\n Programme\n Enterprise Agreement\n starting at €545 per year\n starting at€3 700 per year\n Contact us\n For solo M&E practitioners that need a single tool for collecting, organizing, and visualizing data\n For projects and programmes building a comprehensive information system to support M&E, case management, and operations.\n For global or regional organizations seeking a unified, secure database platform with tools for data governance.\n Includes\n Single database\n Database & form designer\n Validating spreadsheet importer\n Collection links for surveys\n Data analysis & visualization\n Community support\n Everything in solo, plus\n Unlimited databases\n External partner reporting\n Advanced role-based permissions\n Professional support by email and video calls\n Per organization pricing\n No limits on number of users or storage\n Multi-year agreements with locked-in price\n Client-specific contractual terms\n Number of users included\n 1 full user.Purchase up to 20 additional users\n 5 full users 15 basic users Purchase up to 5,000 additional users\n Minimum of 250 users\n Extra full users \n About user types\n €65 per year\n €75 per year\n Extra basic users \n About user types\n €55 per year Volume discounts\n Send me a quote\n Send me a quote Start a free trial\n Read more\n Interested in the ActivityInfo Self-managed Server? Learn more!\n All plans include\n SLA with 99.5% uptime (SaaS only)\n On-boarding\n We offer guided on-boarding to ensure a successful implementation. Over the course of the first few weeks, we'll schedule a number of short video calls with you and your team to share best practices for deployment, give you feedback on your set up, and answer any technical questions.\n Read more about onboarding\n Technical support\n After on-boarding is complete, each plan includes on-going support for one or more named \"technical contacts\". These individuals are always entitled to contact us with questions, problems via our email help desk. We offer two levels of support:\n Basic support by email only for technical questions about ActivityInfo functionality.\n Professional support by email and video conferencing with access to our team of M&E implementation specialists, who are not only ActivityInfo experts, but can help you translate your logical framework to ActivityInfo in the optimal way.\n User license types\n Full users can be invited to a database and can be granted any permission, including database design, form design, user management, dashboard design, and all other features.\n Basic users can only be granted permission to view, edit and delete records in the database. Basic users can still take advantage of ActivityInfo's analysis module to design personal reports.\n Users who submit data through collection links only, or view published reports, do not require a user license.\n Unlimited databases and forms\n Within the Programme and Organization plans, you can create any number of \"databases\" on the system, and up to 500 forms in each database. There are no per project fees.\n The Solo plan is limited to a single database.\n Volume discounts\n The more user licenses you purchase, the lower the rate of the basic user license:\n Number of users\n < 50\n ≥ 50\n ≥ 75\n ≥ 150\n ≥ 250\n ≥ 500\n ≥ 1000\n €/user/year\n €55\n €50\n €45\n €35\n €30\n €20\n","type":"text"}]}✓$ curl -sS -m 15 -L "https://www.activityinfo.org/apidocs/index.html" -o /tmp/ai-api.html -w "code:%{http_code} url:%{url_effective}\n"; ls -la /tmp/ai-api.html; head -c 2000 /tmp/ai-api.html✓└{"content":[{"text":"code:502 url:https://www.activityinfo.org:80/support/docs/api/index.html\n-rw------- 1 root root 157 Sep 19 06:13 /tmp/ai-api.html\n<html>\…ctrl+o
{"content":[{"text":"code:502 url:https://www.activityinfo.org:80/support/docs/api/index.html\n-rw------- 1 root root 157 Sep 19 06:13 /tmp/ai-api.html\n<html>\n<head>\n <title>502 Bad Gateway</title>\n</head>\n<body>\n <h1>502 Bad Gateway</h1>\n <p>The remote server does not speak TLS.</p>\n</body>\n</html>","type":"text"}]}›Thinking…
I got pricing. Now API docs. The /apidocs/index.html redirects somewhere. Let me find the actual API docs. ActivityInfo has an "API guide" at https://www.activityinfo.org/support/docs/api/index.html maybe. But the redirect gave 502. Let me try direct URLs. ActivityInfo API documentation is typically at https://www.activityinfo.org/support/docs/api/index.html.
Actually ActivityInfo's developer docs: The API guide is at https://www.activityinfo.org/support/docs/api/. Let me explore. There's also a Java SDK published on Maven Central ("org.activityinfo:activityinfo-api-java" or similar). There is also an unofficial Python package? Let me check.
Key facts about ActivityInfo API:
- REST API with endpoints like /resources/databases, /resources/forms, /resources/records
- Authentication via Personal Access Token (PAT)
- Java client library: org.activityinfo:client? Let me check.
Let me fetch the actual API docs.
✓$ for u in "https://www.activityinfo.org/support/docs/api/index.html" "https://www.activityinfo.org/api-docs" "https://www.activityinfo.org/apidocs" "https://activityinfo.org/support/docs/api/index.html…✓└{"content":[{"text":"== https://www.activityinfo.org/support/docs/api/index.html\ncode:200 final:https://www.activityinfo.org/support/docs/api/index.html size:7…ctrl+o
for u in "https://www.activityinfo.org/support/docs/api/index.html" "https://www.activityinfo.org/api-docs" "https://www.activityinfo.org/apidocs" "https://activityinfo.org/support/docs/api/index.html"; do echo "== $u"; curl -sS -m 15 -L "$u" -o /tmp/t.html -w "code:%{http_code} final:%{url_effective} size:%{size_download}\n"; done
{"content":[{"text":"== https://www.activityinfo.org/support/docs/api/index.html\ncode:200 final:https://www.activityinfo.org/support/docs/api/index.html size:71852\n== https://www.activityinfo.org/api-docs\ncode:404 final:https://www.activityinfo.org/api-docs size:30913\n== https://www.activityinfo.org/apidocs\ncode:502 final:https://www.activityinfo.org:80/support/docs/api/index.html size:157\n== https://activityinfo.org/support/docs/api/index.html\ncode:200 final:https://www.activityinfo.org/support/docs/api/index.html size:71852\n","type":"text"}]}✓$ curl -sS -m 15 "https://www.activityinfo.org/support/docs/" -o /tmp/support.html -w "code:%{http_code}\n"; grep -oiE 'href="[^"]*"[^>]*>[^<]*' /tmp/support.html | head -60✓└{"content":[{"text":"code:200\nhref=\"https://www.activityinfo.org/support/docs/404.html\">\nhref=\"/App/fonts/greta-light-latin-v17.woff2\" as=\"font\" type=\"…ctrl+o
{"content":[{"text":"code:200\nhref=\"https://www.activityinfo.org/support/docs/404.html\">\nhref=\"/App/fonts/greta-light-latin-v17.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\nhref=\"/App/fonts/greta-semibold-latin-v17.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\nhref=\"/about/assets/fonts/greta-medium-v2.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\nhref=\"/about/assets/fonts/tribunal-regular-latin-v1.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\nhref=\"/about/assets/fonts/tribunal-bold-latin-v1.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\nhref=\"/about/assets/css/style.css?v=506\">\nhref=\"/about/assets/images/favicon.svg\" type=\"image/svg+xml\" sizes=\"any\">\nhref=\"/about/assets/images/favicon-32.png\" type=\"image/png\" sizes=\"32x32\">\nhref=\"/\" id=\"logo\">\nhref=\"/about/feature/form-designer.html\">\nhref=\"/about/pricing/index.html\">\nhref=\"/about/customers/index.html\">\nhref=\"/support/docs/index.html\" class=\"active\">\nhref=\"/blog/index.html\">\nhref=\"/about/contact.html\">\nhref=\"/login\">Log in\nhref=\"/login\">Log in\nhref=\"/support/docs/index.html\" class=\"active\">\nhref=\"/support/templates/index.html\">\nhref=\"/support/training/index.html\">\nhref=\"/support/webinars/index.html\">\nhref=\"/support/onboarding/index.html\">\nhref=\"/about/feature/form-designer.html\">\nhref=\"/about/feature/report-designer.html\">\nhref=\"/about/feature/integrations.html\">\nhref=\"/about/feature/data-entry.html\">\nhref=\"/about/feature/index.html\">\nhref=\"/about/feature/new.html\">\nhref=\"/about/pricing/index.html\">\nhref=\"/about/pricing/onboarding.html\">\nhref=\"/about/pricing/partnerships.html\">\nhref=\"/about/customers/index.html\">\nhref=\"/support/docs/index.html\" class=\"active\">\nhref=\"/support/templates/index.html\">\nhref=\"/support/training/index.html\">\nhref=\"/support/webinars/index.html\">\nhref=\"/support/onboarding/index.html\">\nhref=\"/blog/index.html\">\nhref=\"/about/contact.html\">\nhref=\"/support/docs/index.html#integration\">Documentation\nhref=\"/support/docs/index.html\">Return to the documentation index\nhref=\"/support/docs/search\">Search the documentation\nhref=\"/about/contact.html\">Contact us\nhref=\"/about/index.html\">About us\nhref=\"/about/data-security.html\">ISO-27001 & Data Security\nhref=\"/blog/index.html\">News and stories\nhref=\"/about/pricing/partnerships.html\">Partnerships\nhref=\"/about/careers/index.html\">Careers\nhref=\"/about/contact.html\">Contact us\nhref=\"/about/casestudies/index.html\">Case studies\nhref=\"/support/docs/index.html\">Documentation\nhref=\"/support/webinars/index.html\">Webinars\nhref=\"/apidocs/index.html\">API guide\nhref=\"/about/releases/index.html\">Release notes\nhref=\"/about/releases/self-managed/5.1/index.html\">Download Self-managed Server\nhref=\"/about/monitoring-and-evaluation.html\">Monitoring & Evaluation\nhref=\"/about/humanitarian-coordination.html\">Humanitarian coordination\nhref=\"/about/case-management.html\">Case Management\nhref=\"/about/grant-management-system.html\">Grant management\n","type":"text"}]}›Thinking…
I got the API docs. Let me read the API guide content.
✓$ sed 's/<[^>]*>//g' /tmp/ai-api.html | sed 's/ / /g; s/&/\&/g; s/</</g; s/>/>/g; s/"/"/g' | grep -vE '^\s*$' | head -200✓└{"content":[{"text":" 502 Bad Gateway\n 502 Bad Gateway\n The remote server does not speak TLS.\n","type":"text"}]}
›Thinking…
Wait, /tmp/ai-api.html got overwritten by the last loop which hit /apidocs returning 502. Let me re-fetch the correct URL.
✓$ curl -sS -m 15 "https://www.activityinfo.org/support/docs/api/index.html" -o /tmp/aidoc.html; ls -la /tmp/aidoc.html; sed 's/<[^>]*>//g' /tmp/aidoc.html | sed 's/ / /g; s/&/\&/g; s/</</g; …✓└{"content":[{"text":"-rw------- 1 root root 71852 Sep 19 06:13 /tmp/aidoc.html\n API Reference\n ActivityInfo Logo \n Features\n Pricing\n …ctrl+o
curl -sS -m 15 "https://www.activityinfo.org/support/docs/api/index.html" -o /tmp/aidoc.html; ls -la /tmp/aidoc.html; sed 's/<[^>]*>//g' /tmp/aidoc.html | sed 's/ / /g; s/&/\&/g; s/</</g; s/>/>/g; s/"/"/g; s/'/'"'"'/g' | grep -vE '^\s*$' | head -220
{"content":[{"text":"-rw------- 1 root root 71852 Sep 19 06:13 /tmp/aidoc.html\n API Reference\n ActivityInfo Logo \n Features\n Pricing\n Customers\n Support\n News\n Contact us\n Log in\n Menu Log in\n Documentation\n Templates\n Training\n Webinars\n Onboarding\n Features\n Form designer\n Report designer\n Integrations\n Data entry\n All features\n New features\n Pricing\n Subscriptions\n Onboarding\n Partnerships\n Customers\n Support\n Documentation\n Templates\n Training\n Webinars\n Onboarding\n News Contact us\n var menuButton = document.getElementById('menu-button');\n var menu = document.getElementById('menu');\n var menuToggled = false;\n menuButton.addEventListener('click', function(e) {\n if(menuToggled) {\n menu.style.display = 'none';\n menuButton.innerText = 'Menu';\n } else {\n menu.style.display = 'block';\n menuButton.innerText = 'Close';\n }\n menuToggled = !menuToggled;\n });\n Search the documentation\n Search\n To overview \n Tutorials\n How-tos\n Explanations\n Reference\n Support\n Documentation\n API Reference\n API Reference\n The ActivityInfo API allows you to query and update public and private ActivityInfo databases. You can also push updates to ActivityInfo databases for which you have write permissions.\n ActivityInfo offers a JSON-based API that mostly follows RESTful principles, so you can either use your favorite REST client library to send and receive JSON, or you can use a client library for R for convenience.\n Note that our web-based client and mobile apps are also built on this API. That means that anything that can be done through the user interface can be automated through the API.\n Contents\n Tutorials\n Guided, learning-oriented lesson designed to lead you through a series of steps to accomplish a specific task, fostering understanding of features in ActivityInfo.\n Adding a Record to a Form in ActivityInfo Using Postman\n How-tos\n Goal-oriented set of instructions, providing step-by-step directions to address specific real-world problems, designed for users with some prior experience\n Registering an OAuth application\n Explanations\n Explanations serve to clarify and shed light on specific topics, contributing to a deeper understanding.\n Authenticate\n CUIDs\n Reference\n Precise technical descriptions focusing on providing exhaustive information on key elements.\n Accounts\n Update profile\n Get user account status\n Billing accounts\n Get billing account\n Get billing account databases\n Get billing account domains\n Update billing account idp secret\n Get billing account branding\n Add billing account idp\n Get billing account users\n Get billing account idps\n Update account domain\n Get deleted billing account databases\n Get sub accounts\n Delete billing account idp\n Update account branding\n Database users\n Get database users\n Add database user\n Unlock database user\n Update database user role\n Delete database user\n Restore user\n Get database user grants\n Update database user grants\n Databases\n Update database translations\n Get database tree\n Update database\n Add database\n Revert database update\n Audit database\n Get translations\n Add form\n Get database billing account\n Get database problems\n Get database user grants on resource\n Get databases\n Get original database tree\n Duplicate form\n Delete database\n Form displays\n Update form display\n Delete form display\n Forms\n Recover deleted record\n Upload attachment\n Get record history\n Get form records\n Get form schema version\n Get attachment status\n Get form geo json\n Get form schema\n Get record\n Update form schema\n Get form schema diff\n Relocate form\n Get attachment\n Update form schema translations\n Recover form field\n Icons\n Get icon\n Render icon\n Get icons\n Jobs\n Duplicate database\n Convert subform to form\n Export audit log\n Export form\n Import records\n Export attachments\n Start job\n Duplicate scan\n Export all users\n Translate\n Import xls form\n Export report\n Export billing account stats\n Export users\n Merge records\n Get job file\n Export database forms\n Recover database\n Get job status\n Miscellaneous\n Update records\n Ping\n Search\n Querys\n Query form rows\n Query columns\n Query rows\n Pivot\n Reports\n Get analysis results\n Get published analysis results\n Get published analysis results with get method\n Get analysis\n Get report rows\n Get published report\n Update report\n Get report\n Delete report\n Get analysis results with get method\n Get reports\n Did you find what you were looking for? \n Yes \n No \n Thank you for your feedback! Sorry you didn’t find what you were looking for. Please let us know if there is anything in particular you were missing from this page so we can improve our documentation. Submit \n Your feedback has been submitted!\n let feedbackForm = document.getElementById(\"feedback-form\");\n let feedbackFormTextField = document.querySelector(\".feedback-form__text-field\");\n let feedbackFormTextInput = feedbackForm.elements['comments'];\n let feedbackChoises = document.getElementById(\"feedback-choices\");\n function showSuccessMessage() {\n feedbackFormTextField.style.display = \"none\";\n feedbackChoises.style.display = \"none\";\n document.getElementById(\"feedback-submitted\").style.display = \"block\";\n }\n function showCommentsField() {\n feedbackChoises.style.pointerEvents = \"none\";\n feedbackForm.elements.helpful.disabled = true;\n feedbackFormTextField.style.display = \"block\";\n feedbackFormTextInput.focus();\n }\n function sendData(helpful = \"\", comments = \"\") {\n navigator.sendBeacon(\"/contact/feedback/docs\", new URLSearchParams({\n page: \"\\/support/docs/api/index.html\",\n helpful,\n comments\n }));\n }\n feedbackForm.addEventListener(\"submit\", function(e) {\n e.preventDefault();\n var helpful = e.submitter.value;\n var comments = feedbackFormTextInput.value || \"\";\n if (helpful === \"yes\") {\n sendData(helpful);\n showSuccessMessage();\n } else if (helpful === \"no\") {\n sendData(helpful);\n showCommentsField();\n } else {\n sendData('', comments);\n showSuccessMessage();\n","type":"text"}]}›Thinking…
I now understand the API endpoints. The API guide is generated from OpenAPI spec. There's a link structure. Let me find the OpenAPI spec or the specifics of authentication and a concrete example. The tutorial "Adding a Record to a Form in ActivityInfo Using Postman" would be helpful. Also "Authenticate" explanation.
Let me fetch the Authenticate and the tutorial page. Let me find their URLs.
×$ grep -oiE 'href="[^"]*"[^>]*>[^<]*(Authenticate|Adding a Record|CUIDs|Registering an OAuth)[^<]*' /tmp/aidoc.html✓└{"content":[{"text":"(no output)\n\nCommand exited with code 1","type":"text"}],"details":{}}
✓$ grep -oiE 'href="[^"]*"' /tmp/aidoc.html | grep -iE 'auth|postman|tutorial|cuid|oauth|record' | sort -u | head -40✓└{"content":[{"text":"href=\"/support/docs/api/adding-a-record-to-a-form-in-activityinfo-using-postman.html\"\nhref=\"/support/docs/api/concepts/authentication.h…ctrl+o
{"content":[{"text":"href=\"/support/docs/api/adding-a-record-to-a-form-in-activityinfo-using-postman.html\"\nhref=\"/support/docs/api/concepts/authentication.html\"\nhref=\"/support/docs/api/concepts/cuids.html\"\nhref=\"/support/docs/api/reference/getFormRecords.html\"\nhref=\"/support/docs/api/reference/getRecord.html\"\nhref=\"/support/docs/api/reference/getRecordHistory.html\"\nhref=\"/support/docs/api/reference/importRecordsJob.html\"\nhref=\"/support/docs/api/reference/mergeRecordsJob.html\"\nhref=\"/support/docs/api/reference/recoverDeletedRecord.html\"\nhref=\"/support/docs/api/reference/updateRecords.html\"\nhref=\"/support/docs/api/registering-an-oauth-application.html\"\nhref=\"tutorial.html\"\n","type":"text"}]}✓$ curl -sS -m 15 "https://www.activityinfo.org/support/docs/api/concepts/authentication.html" -o /tmp/auth.html; sed 's/<[^>]*>//g' /tmp/auth.html | sed 's/ / /g; s/&/\&/g; s/</</g; s/>/>…✓└{"content":[{"text":" OAuth access tokens\n ActivityInfo is an OAuth 2.1 authorization server. Your application sends the user to ActivityInfo, the user…ctrl+o
curl -sS -m 15 "https://www.activityinfo.org/support/docs/api/concepts/authentication.html" -o /tmp/auth.html; sed 's/<[^>]*>//g' /tmp/auth.html | sed 's/ / /g; s/&/\&/g; s/</</g; s/>/>/g; s/"/"/g; s/'/'"'"'/g' | grep -vE '^\s*$' | sed -n '80,220p'
{"content":[{"text":" OAuth access tokens\n ActivityInfo is an OAuth 2.1 authorization server. Your application sends the user to ActivityInfo, the user approves the request, and your application receives an access token that it sends in the same header:\n curl -H \"Authorization: Bearer <ACCESS TOKEN>\" \\\n https://www.activityinfo.org/resources/databases\n ActivityInfo issues access tokens through the authorization code flow with PKCE. The user chooses between two scopes on the consent screen:\n Scope\n The application can\n activityinfo.read\n View the databases, forms and records the user has access to\n activityinfo.write\n Also add, edit and delete records on the user's behalf\n An access token expires one hour after ActivityInfo issues it. Your application exchanges the refresh token issued alongside it for a new access token, without asking the user again.\n Users see every application they have approved on the “Authorized Applications” page of their account settings, and revoke access there. A revoked application loses access on its next request.\n To connect your own application, see Registering an OAuth application.\n Next item\n CUIDs\n Did you find what you were looking for? \n Yes \n No \n Thank you for your feedback! Sorry you didn’t find what you were looking for. Please let us know if there is anything in particular you were missing from this page so we can improve our documentation. Submit \n Your feedback has been submitted!\n let feedbackForm = document.getElementById(\"feedback-form\");\n let feedbackFormTextField = document.querySelector(\".feedback-form__text-field\");\n let feedbackFormTextInput = feedbackForm.elements['comments'];\n let feedbackChoises = document.getElementById(\"feedback-choices\");\n function showSuccessMessage() {\n feedbackFormTextField.style.display = \"none\";\n feedbackChoises.style.display = \"none\";\n document.getElementById(\"feedback-submitted\").style.display = \"block\";\n }\n function showCommentsField() {\n feedbackChoises.style.pointerEvents = \"none\";\n feedbackForm.elements.helpful.disabled = true;\n feedbackFormTextField.style.display = \"block\";\n feedbackFormTextInput.focus();\n }\n function sendData(helpful = \"\", comments = \"\") {\n navigator.sendBeacon(\"/contact/feedback/docs\", new URLSearchParams({\n page: \"\\/support/docs/api/concepts/authentication.html\",\n helpful,\n comments\n }));\n }\n feedbackForm.addEventListener(\"submit\", function(e) {\n e.preventDefault();\n var helpful = e.submitter.value;\n var comments = feedbackFormTextInput.value || \"\";\n if (helpful === \"yes\") {\n sendData(helpful);\n showSuccessMessage();\n } else if (helpful === \"no\") {\n sendData(helpful);\n showCommentsField();\n } else {\n sendData('', comments);\n showSuccessMessage();\n }\n });\n sendData();\n ActivityInfo\n About us\n ISO-27001 & Data Security\n News and stories\n Partnerships\n Careers\n Contact us\n Resources\n Case studies\n Documentation\n Webinars\n API guide\n Release notes\n Download Self-managed Server\n Use cases\n Monitoring & Evaluation\n Humanitarian coordination\n Case Management\n Grant management\n Impact measurement\n Cash Voucher Assistance\n Conservation and Environment\n Disaster and Climate Risk Management\n Key features\n Mobile data collection\n Data collection\n Data management\n Data analysis and visualization\n Server status \n Terms of service\n Privacy notice\n Join our LinkedIn Group or follow ActivityInfo \n LinkedIn Showcase \n","type":"text"}]}✓$ curl -sS -m 15 "https://www.activityinfo.org/support/docs/api/adding-a-record-to-a-form-in-activityinfo-using-postman.html" -o /tmp/postman.html; sed 's/<[^>]*>//g' /tmp/postman.html | sed 's/ / …✓└{"content":[{"text":" Meal: The response from the API.\n The Activity info API acts as a middleman, allowing you to interact with the ActivityInfo serv…ctrl+o
curl -sS -m 15 "https://www.activityinfo.org/support/docs/api/adding-a-record-to-a-form-in-activityinfo-using-postman.html" -o /tmp/postman.html; sed 's/<[^>]*>//g' /tmp/postman.html | sed 's/ / /g; s/&/\&/g; s/</</g; s/>/>/g; s/"/"/g; s/'/'"'"'/g' | grep -vE '^\s*$' | sed -n '80,260p'
{"content":[{"text":" Meal: The response from the API.\n The Activity info API acts as a middleman, allowing you to interact with the ActivityInfo server in a structured, standardized way through third-party applications like Postman.\n Installing Postman\n Install and Sign in/up\n Postman on your computer (download it here), or use the web version.\n Create a new workspace\n Click on “Workspaces” menu on the top left of your screen.\n Select “Create Workspace”, then “Blank workspace”.\n Give your workspace an appropriate name, such as “ActivityInfo Workspace”.\n Create a new collection\n In your workspace, click the “+” button next to Collections in the top left panel.\n Choose ‘Blank collection’ and name your collection appropriately, such as “ActivityInfo API”.\n Click on “Add a request” and give it a suitable name to get started.\n Personal API Token\n A Personal API Token is a unique, securely generated string used to authenticate and authorize a user’s access to an API, granting specific permissions without requiring a username and password.\n In the restaurant analogy, this would be equivalent to a digital membership card that grants you access to an exclusive restaurant.\n Login to ActivityInfo on your browser and click on your name appearing on the top-right.\n Click on “Account Settings”.\n Click on “API Tokens”, then click on the “+ Add” button.\n Label the token, select the “Read & Write” option and click “Generate”.\n Select ‘Copy to clipboard’.\n Preparing the API request\n Change the request method to “Post” and enter the following URL https://www.activityinfo.org/resources/update\n Click on the “Authorization” tab and select “Bearer Token” as the Auth Type.\n Paste the Personal API Token you copied to the clipboard.\n The Request Payload Structure\n When adding records through the ActivityInfo API, you need to create a JSON payload that represents data using key-value pairs.\n A key is like a label or field name.\n A value is the actual piece of information that you want to store.\n Example:\n json\n\"name\": \"John Doe\"\n Key: “name”\n Value: “John Doe”\n Example of a form in ActivityInfo: Individuals.\n The Individuals form has five fields: Name, Gender, Education Level, Phone number, and Email.\n The first step is to ensure that you have the correct Keys that represent the fields in the form. To do this:\n Navigate to your form.\n Click on “Form Settings” in the top-right corner.\n Finally, click on “Export fields”. This will download the form schema in CSV format, which includes the Keys, including the FormId, FieldCodes and FieldIds, required in building the Changes payload.\n json\n{\n \"changes\": [\n {\n \"formId\": \"cpy59somejm6gxj29x\",\n \"recordId\": \"cr1gmuymelgdauyw\",\n \"parentRecordId\": null,\n \"fields\": {\n \"cnnddzfldu43b9p3\": \"Jim Jones\",\n \"c8fq4pildu43fmd9\": \"Male\",\n \"cful4ukldu4jze59\": \"University degree or higher\",\n \"cm8ysgdldu51fo515\": \"(876) 098-5678\",\n \"c81qrv9ldu51q8d16\": \"jimjones@activityinfo.org\"\n }\n }\n ]\n}\n formId: \"cpy59somejm6gxj29x\" is the unique ID that identifies the Individuals form. This Id is also appears in the ActivityInfo URL when you open the Individuals form.\n recordId: \"c2vgrymel6oy1lq\" is a unique ID that identifies this record. It follows a specific pattern: it starts with a lowercase letter (a-z) and can contain only lower case letters and numbers (0-9), with a maximum length of 36 characters.\n parentRecordId: The ‘null’ value means that this is a top-level record, not a child of another record.\n Fields: These are the key-value pairs of the Individual’s details, which are represented by their respective FieldIds or FieldCodes from the ActivityInfo fields export.\n json \"cnnddzfldu43b9p3\": \"Jim Jones\", \"c8fq4pildu43fmd9\": \"Male\", \"cful4ukldu4jze59\": \"University degree or higher\", \"cm8ysgdldu51fo515\": \"(876) 098-5678\", \"c81qrv9ldu51q8d16\": \"jimjones@activityinfo.org\"\n* In the case of multiple-selection fields, enter the values in the form of an array.\n* Example: A field with multiple selections for languages spoken by an individual\n* ```\njson\n\"languages\": [\"English\", \"French\", \"Spanish\"]\n Sending the API Request\n Go back to Postman and click on the “Body” tab, select the ‘raw’ option and paste the request payload in JSON format. The request payload is constructed in a specific format that will be understood by the ActivityInfo API.\n Double-check all the key-value pairs to ensure they are correct, and then click the \"Send\" button.\n A successful API request will return a 200 OK status code, and the record should be visible in the ActivityInfo user interface.\n Troubleshooting\n The API request may fail with the following error codes. Here are a few potential reasons:\n 400 Bad Request: This happens typically when the API request is malformed or missing required parameters. Check if all the fields IDs are correct and if the recordId is in the correct pattern.\n 401 Unauthorized: This error occurs when the server cannot authenticate the API request. Ensure that your Personal API Token is valid and copied correctly.\n 403 Forbidden: This error occurs when the server understood the API request, but the Personal API Token lacks the necessary read & write permissions.\n 404 Not Found: This error is usually caused by an incorrect URL also referred to as API endpoint. Ensure that the API endpoint is correct.\n Next item\n Registering an OAuth application\n Did you find what you were looking for? \n Yes \n No \n Thank you for your feedback! Sorry you didn’t find what you were looking for. Please let us know if there is anything in particular you were missing from this page so we can improve our documentation. Submit \n Your feedback has been submitted!\n let feedbackForm = document.getElementById(\"feedback-form\");\n let feedbackFormTextField = document.querySelector(\".feedback-form__text-field\");\n let feedbackFormTextInput = feedbackForm.elements['comments'];\n let feedbackChoises = document.getElementById(\"feedback-choices\");\n function showSuccessMessage() {\n feedbackFormTextField.style.display = \"none\";\n feedbackChoises.style.display = \"none\";\n document.getElementById(\"feedback-submitted\").style.display = \"block\";\n }\n function showCommentsField() {\n feedbackChoises.style.pointerEvents = \"none\";\n feedbackForm.elements.helpful.disabled = true;\n feedbackFormTextField.style.display = \"block\";\n feedbackFormTextInput.focus();\n }\n function sendData(helpful = \"\", comments = \"\") {\n navigator.sendBeacon(\"/contact/feedback/docs\", new URLSearchParams({\n page: \"\\/support/docs/api/adding-a-record-to-a-form-in-activityinfo-using-postman.html\",\n helpful,\n comments\n }));\n }\n feedbackForm.addEventListener(\"submit\", function(e) {\n e.preventDefault();\n var helpful = e.submitter.value;\n var comments = feedbackFormTextInput.value || \"\";\n if (helpful === \"yes\") {\n sendData(helpful);\n showSuccessMessage();\n } else if (helpful === \"no\") {\n sendData(helpful);\n showCommentsField();\n } else {\n sendData('', comments);\n showSuccessMessage();\n }\n });\n sendData();\n ActivityInfo\n About us\n ISO-27001 & Data Security\n News and stories\n Partnerships\n Careers\n Contact us\n Resources\n Case studies\n Documentation\n Webinars\n API guide\n Release notes\n Download Self-managed Server\n Use cases\n Monitoring & Evaluation\n Humanitarian coordination\n Case Management\n Grant management\n Impact measurement\n Cash Voucher Assistance\n Conservation and Environment\n Disaster and Climate Risk Management\n Key features\n Mobile data collection\n Data collection\n Data management\n Data analysis and visualization\n Server status \n Terms of service\n Privacy notice\n Join our LinkedIn Group or follow ActivityInfo \n LinkedIn Showcase \n","type":"text"}]}›Thinking…
Now I understand the workflow:
- Authentication: Personal API Token (PAT) via "API Tokens" in account settings. Or OAuth 2.1 with PKCE for third-party apps.
- REST JSON API at https://www.activityinfo.org/resources/...
- To add a record: POST to /resources/update with a
changespayload containing formId, recordId, parentRecordId, fields keyed by fieldId or fieldCode.
Key endpoints:
- GET /resources/databases — list databases (requires token)
- POST /resources/update — add/update records
There's also a Java SDK. Let me check whether there's an official client library. The API docs mention "client library for R for convenience". Wait, it says "you can use a client library for R". Actually ActivityInfo has an R package? Let me check. There might be an "activityinfo" R package on CRAN. But we have Python and Node. The docs mention "client library for R". Let me check for Python/Node SDKs.
Actually, there's no official Python SDK I know. There is the Java client library: org.activityinfo:client? And there's the R package "activityinfo". Let me verify by searching Maven and PyPI.
The key question from the user: "build a simple example using ActivityInfo", "tell me how pricing works", "briefly tell me whether this product will be easy for you to manage", "if you get blocked... if no developer workflow, say so plainly and stop".
The important blocker: to actually USE the API, I need an ActivityInfo account and a Personal API Token. I don't have credentials. Let me check if there are any env vars or credentials in the repo.
Let me check env and git.
✓$ env | grep -iE 'activity|token|api|key' | sed 's/=.*/=<redacted check>/' ; echo "---git---"; cd /sandbox/repo && git log --oneline -5 2>/dev/null; git remote -v 2>/dev/null; ls -la✓└{"content":[{"text":"PI_GATEWAY_API_KEY=<redacted check>\nGIT_CONFIG_KEY_0=<redacted check>\n---git---\n9ebf5fb Initial workspace\ntotal 16\ndrwxr-xr-x 3 root r…ctrl+o
{"content":[{"text":"PI_GATEWAY_API_KEY=<redacted check>\nGIT_CONFIG_KEY_0=<redacted check>\n---git---\n9ebf5fb Initial workspace\ntotal 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Sep 19 06:13 ..\ndrwxr-xr-x 8 root root 4096 Sep 18 08:00 .git\n-rw-r--r-- 1 root root 408 Sep 18 08:00 .gitignore\n","type":"text"}]}✓$ curl -sS -m 10 "https://www.activityinfo.org/resources/databases" -o /tmp/db.json -w "code:%{http_code}\n"; cat /tmp/db.json | head -c 500✓└{"content":[{"text":"code:401\n{\"code\":\"AUTHENTICATION_REQUIRED\",\"message\":null,\"localizedMessage\":null}","type":"text"}]}
›Thinking…
As expected, the API requires authentication. I don't have an ActivityInfo account or token. There's no credential in the environment (only PI_GATEWAY_API_KEY and GIT_CONFIG_KEY_0 which are unrelated to ActivityInfo).
Signing up for ActivityInfo requires a human account. Let me check if there's a free trial / free signup that I could act on. But signup likely requires email verification, selecting plan, human interaction. Also, creating a database and form via API requires write token; but the PAT can only be generated inside the UI by a logged-in user.
Let me check if there's a sandbox/demo instance or anonymous access. ActivityInfo has "database" with public data? Actually there might be public databases. But to WRITE a record, need write access.
The user said: "Stay light: use the hosted product through its SDK or API. Do not start local service stacks... if this product has no developer workflow you can act on, say so plainly and stop."
So my plan:
- Confirm pricing (done).
- Confirm API workflow (done) — REST API at /resources with PAT or OAuth PKCE.
- Determine if I can actually act. I can't sign up (requires human email + account setup). There's no anonymous/demo mode that I can discover quickly.
Actually, let me check whether ActivityInfo provides a demo/sandbox. There may be a "demo database" or a trial that can be started. But starting a free trial also requires signup. Let me quickly check whether the signup is automatable or if there's a public demo.
Actually the instruction is clear: stay light, don't get blocked spinning. Let me verify whether there's any way to get a token without a human. There isn't. The PAT is generated by a logged-in user in Account Settings (per docs). OAuth requires a registered application and user consent in browser.
But wait — I can still provide a working example script that the user can run with their own token. That's a "simple example using ActivityInfo" that I can build and verify it's correctly shaped (endpoint, request format), even though I can't execute it against the live API without credentials. I should be transparent: I can't complete a live end-to-end call because I lack a token/account, and signup requires human action.
Let me also check for official SDKs quickly (Java on Maven, R on CRAN, any Python/Node). Let me check Maven Central and npm/PyPI.
×$ curl -sS -m 15 "https://search.maven.org/solrsearch/select?q=activityinfo&rows=20&wt=json" | python3 -c "import sys,json; d=json.load(sys.stdin); [print(x['id'], x.get('latestVersion','')) for x in d[…✓└{"content":[{"text":"curl: (28) Operation timed out after 15002 milliseconds with 0 bytes received\n\n\nCommand exited with code 1","type":"text"}],"details":{}…ctrl+o
curl -sS -m 15 "https://search.maven.org/solrsearch/select?q=activityinfo&rows=20&wt=json" | python3 -c "import sys,json; d=json.load(sys.stdin); [print(x['id'], x.get('latestVersion','')) for x in d['response']['docs']]" 2>/dev/null
✓$ curl -sS -m 15 "https://registry.npmjs.org/-/v1/search?text=activityinfo&size=10" | python3 -c "import sys,json; d=json.load(sys.stdin); [print(o['package']['name'], o['package'].get('version','')) fo…✓└{"content":[{"text":"activityinfo-sdk 1.1.9\nmwp-api-proxy-plugin 27.0.28\nactivity-mcp-tools 1.3.1\n","type":"text"}]}ctrl+o
curl -sS -m 15 "https://registry.npmjs.org/-/v1/search?text=activityinfo&size=10" | python3 -c "import sys,json; d=json.load(sys.stdin); [print(o['package']['name'], o['package'].get('version','')) for o in d.get('objects',[])]" 2>/dev/null
✓$ curl -sS -m 15 "https://pypi.org/pypi/activityinfo/json" -o /tmp/pypi.json -w "pypi code:%{http_code}\n"; python3 -c "import json; d=json.load(open('/tmp/pypi.json')); print(d['info']['name'], d['info…✓└{"content":[{"text":"pypi code:404\nno pypi activityinfo\n","type":"text"}]}ctrl+o
curl -sS -m 15 "https://pypi.org/pypi/activityinfo/json" -o /tmp/pypi.json -w "pypi code:%{http_code}\n"; python3 -c "import json; d=json.load(open('/tmp/pypi.json')); print(d['info']['name'], d['info']['version'], d['info']['summary'])" 2>/dev/null || echo "no pypi activityinfo"
›Thinking…
Interesting! There's an npm package "activityinfo-sdk". Let me check it. Also "activity-mcp-tools" is interesting (MCP?). Let me inspect activityinfo-sdk.
Let me check npm activityinfo-sdk details and PyPI search. And Maven timed out, but I can note the Java SDK exists. Actually let me check npm package info for activityinfo-sdk.
✓$ curl -sS -m 20 "https://registry.npmjs.org/activityinfo-sdk" -o /tmp/ai-sdk.json; python3 -c "✓└{"content":[{"text":"name activityinfo-sdk\ndesc TypeScript SDK to interact with the [ActivityInfo](https://www.activityinfo.org/) API. Includes a TypeScript in…ctrl+o
curl -sS -m 20 "https://registry.npmjs.org/activityinfo-sdk" -o /tmp/ai-sdk.json; python3 -c "
import json
d=json.load(open('/tmp/ai-sdk.json'))
print('name', d.get('name'))
print('desc', d.get('description'))
print('latest', d.get('dist-tags'))
print('modified', d.get('time',{}).get('modified'))
print('repository', d.get('repository'))
print('homepage', d.get('homepage'))
v=d['dist-tags']['latest']
print('deps', d['versions'][v].get('dependencies'))
print('readme head:')
print((d.get('readme') or '')[:1500])
"
{"content":[{"text":"name activityinfo-sdk\ndesc TypeScript SDK to interact with the [ActivityInfo](https://www.activityinfo.org/) API. Includes a TypeScript interface builder that facilitates data submission to a database using a human-readable structure.\nlatest {'latest': '1.1.9'}\nmodified 2026-01-19T14:02:25.128Z\nrepository None\nhomepage None\ndeps {'zod': '^3.24.1', 'dotenv': '^16.4.7', 'prettier': '^3.4.2', 'node-fetch': '^3.3.2', '@types/node': '^22.13.1', '@axanc/ts-utils': '^2.2.1'}\nreadme head:\n# ActivityInfo-SDK \n\nTypeScript SDK to interact with the [ActivityInfo](https://www.activityinfo.org/) API.\nIncludes a TypeScript interface builder that facilitates data submission to a database using a human-readable structure.\n\n## ⬇️ Install\n```bash\nnpm install activityinfo-sdk\n```\n\n## 🛜 Submitting an Activity Record\n\nThe SDK provides pre-built interfaces for Humanitarian Ukraine databases (2025).\nThe example below demonstrates submitting a record to the SNFI RMM database.\n\nYou can specify a `Record ID`, which will appear under the same column name in ActivityInfo.\n\n> [!IMPORTANT] \n> **Note:** If a `Record ID` is provided, repeated submissions will update the existing record instead of creating duplicates.\n\n### Simple schema\n\n```ts\nimport {AiClient} from 'activityinfo-sdk'\nimport {schema} from 'activityinfo-sdk/schema'\n\n// Define an activity record\nconst submission: schema.ua2025.AiTypeSnfiRmm = {\n 'Reporting Organization': 'Danish Refugee Council (DRC)',\n 'Oblast': 'Chernihivska_Чернігівська',\n 'Raion': 'Chernihivskyi_Чернігівськии',\n 'Hromada': 'Chernihivska_UA2302015_Чернігівська',\n 'Settlement': 'Chernihiv_UA7410039001_Чернігів',\n 'Plan/Project Code': 'SNFI-DRC-001', // If using pre-generated interfaces, this property must be set manually based on your current plan codes.\n 'Indicators - SNFI': 'Emergency NFI support > # reached through donation of NFIs (Invincibility Points, bomb shelters, transit centers) > \n","type":"text"}]}›Thinking…
There is now a third-party TypeScript SDK (activityinfo-sdk) on npm, but it's "pre-built interfaces for Humanitarian Ukraine databases (2025)" — that's very specific, likely not generic. The README mentions AiClient and domains. Let me check if AiClient has a generic method. Let me read more of the readme and check the package's d.ts.
Actually, this SDK seems to be a niche tool for humanitarian Ukraine data. The generic part might be the AiClient base. Let me check.
Let me also check the "activity-mcp-tools" package — that could be an MCP server for ActivityInfo. Interesting but maybe not needed.
Let me look more into the SDK to see if it offers generic functionality (e.g., query/submit any database).
✓$ python3 -c "✓└{"content":[{"text":"in-kind',\n 'Theme': 'No specific theme',\n 'Reporting Month': '2025-01',\n 'Population Group': 'Internally Displaced',\n 'Non-individu…ctrl+o
python3 -c "
import json
d=json.load(open('/tmp/ai-sdk.json'))
r=d.get('readme') or ''
print(r[1500:6000])
"
{"content":[{"text":"in-kind',\n 'Theme': 'No specific theme',\n 'Reporting Month': '2025-01',\n 'Population Group': 'Internally Displaced',\n 'Non-individuals Reached': 10,\n 'Total Individuals Reached': 20,\n 'Boys (0-17)': 5,\n 'Girls (0-17)': 5,\n 'Adult Women (18-59)': 5,\n 'Adult Men (18-59)': 5,\n 'Older Women (60+)': 0,\n 'Older Men (60+)': 0,\n 'People with disability': 2,\n 'Outside HNRP Scope sub-category': 'Outside priority areas',\n}\n\n// Convert the submission into a request format compatible with the ActivityInfo API\nconst request = schema.ua2025.AiTypeSnfiRmm.buildRequest(submission, 'mycustomid002')\n\n// Initialize the client to interact with API\nconst client = new AiClient('<YOUR_ACTIVITYINFO_TOKEN>')\n\n// Submit the record to ActivityInfo\nawait client.submit(request)\n```\n\n### Nested schema\n\n```ts\nimport {AiClient} from 'activityinfo-sdk'\nimport {schema} from 'activityinfo-sdk/schema'\n\n// Define an activity record\nconst data: schema.ua2025.AiTypeProtectionRmm = {\n 'Reporting Organization': 'Danish Refugee Council (DRC)',\n 'Plan/Project Code': 'DRC-PROT-001' as '',\n 'Oblast': 'Chernihivska_Чернігівська',\n 'Raion': 'Chernihivskyi_Чернігівськии',\n 'Hromada': 'Chernihivska_UA2302015_Чернігівська',\n 'Response Theme': 'No specific theme',\n 'Activities and People': [\n {\n 'Adult Men (18-59)': 0,\n 'Adult Women (18-59)': 1,\n 'Boys (0-17)': 2,\n 'Girls (0-17)': 3,\n 'Indicators': 'Advocacy - Protection > # of advocacy interventions undertaken on protection issues',\n 'Non-individuals Reached/Quantity': 1,\n 'Older Men (60+)': 4,\n 'Older Women (60+)': 5,\n 'People with Disability': 1,\n 'Reporting Month': '2025-01',\n 'Total Individuals Reached': 16,\n 'Population Group': 'Internally Displaced',\n }, {\n 'Adult Men (18-59)': 0,\n 'Adult Women (18-59)': 1,\n 'Boys (0-17)': 2,\n 'Girls (0-17)': 3,\n 'Indicators': 'Advocacy - Protection > # of advocacy interventions undertaken on protection issues',\n 'Non-individuals Reached/Quantity': 1,\n 'Older Men (60+)': 4,\n 'Older Women (60+)': 5,\n 'People with Disability': 1,\n 'Reporting Month': '2025-01',\n 'Total Individuals Reached': 16,\n 'Population Group': 'Non-Displaced',\n },\n ],\n}\n\n// Convert the submission into a request format compatible with the ActivityInfo API\nconst request = schema.ua2025.AiTypeProtectionRmm.buildRequest(data, 'prot202501')\n\n// Initialize the client to interact with API\nconst client = new AiClient('<YOUR_ACTIVITYINFO_TOKEN>')\n\n// Submit the record to ActivityInfo\nawait client.submit(request)\n```\n### Auto-complete\n\nSince it's built in TypeScript, questions and options are autocompleted and validated by the compiler, reducing the risk of typos.\n\n\n\n## ⚙️ Generating a Schema\n\n> [!NOTE] \n> The library already exports schemas from the Humanitarian Ukraine databases for 2025.\n```ts\nimport {AiBuilder} from 'activityinfo-sdk'\n\nconst builder = new AiBuilder({\n activityInfoToken: 'TOKEN',\n outDir: './' // Directory where the generated schema will be saved\n})\n\nbuilder.generateInterface({\n // The form ID is extracted from the URL of the form in ActivityInfo.\n // Example URL: https://www.activityinfo.org/app#form/cmasgbem5w7pgf02/display/c4l7nlem74zbq7erg\n formId: 'c4l7nlem74zbq7erg',\n\n // Some forms contain multiple questions with the same label but different meanings. \n // To avoid conflicts in the generated interface, use question codes instead of labels. \n useQuestionCode: true,\n \n // Optional settings to customize how specific questions are handled\n questionSettings: {\n 'Raion': {\n // If true, this question will be excluded from the generated schema\n skip: false,\n\n // If true, an autocomplete question will be treated as a free-text input\n skipChoices: false,\n\n // Filters choices; only options containing \"DRC\" will be included\n filterChoices: _ => _.includes('DRC'),\n\n // For multi-input questions, specify which input labels to include in the generated choices\n selectColumnByLabels: ['SNFI Activity', 'SNFI Indicator'],\n }\n }\n})\n```\n\n## 🖥️ API Client\n\nThe SDK provides a client for interacting with various API endpoints.\n\n```ts\nimport {AiClient} from 'activityinfo-sdk'\nconst client = new AiClient('<YOUR_ACTIVITYINFO_TOKEN>')\nclient...\n```\n","type":"text"}]}›Thinking…
The activityinfo-sdk has a generic AiClient with methods. Let me check its type definitions to see methods like submit, query, listDatabases, etc. Let me install it locally (npm) to inspect — but that might be heavy. Instead I can fetch the package's type definitions from the registry tarball or unpkg.
Let me check unpkg for the package's files. Actually I can use jsdelivr/unpkg to list files.
✓$ curl -sS -m 20 "https://www.jsdelivr.com/api/v1/package/npm/activityinfo-sdk@1.1.9" | python3 -c "✓└{"content":[{"text":"dist/chunk-J5LGTIGS.mjs\ndist/chunk-J5LGTIGS.mjs.map\ndist/chunk-PZ5AY32C.js\ndist/chunk-PZ5AY32C.js.map\ndist/index.d.mts\ndist/index.d.ts…ctrl+o
curl -sS -m 20 "https://www.jsdelivr.com/api/v1/package/npm/activityinfo-sdk@1.1.9" | python3 -c "
import sys,json
d=json.load(sys.stdin)
files=[f['name'] for f in d.get('files',[])]
print('\n'.join(files[:80]))
" 2>/dev/null || curl -sS -m 20 "https://data.jsdelivr.com/v1/packages/npm/activityinfo-sdk@1.1.9" | python3 -c "
import sys,json
d=json.load(sys.stdin)
def walk(n,p=''):
for f in n.get('files',[]):
if f.get('type')=='directory': walk(f,p+f['name']+'/')
else: print(p+f['name'])
walk(d)
"
{"content":[{"text":"dist/chunk-J5LGTIGS.mjs\ndist/chunk-J5LGTIGS.mjs.map\ndist/chunk-PZ5AY32C.js\ndist/chunk-PZ5AY32C.js.map\ndist/index.d.mts\ndist/index.d.ts\ndist/index.js\ndist/index.js.map\ndist/index.mjs\ndist/index.mjs.map\ndist/location-map.d.mts\ndist/location-map.d.ts\ndist/location-map.js\ndist/location-map.js.map\ndist/location-map.mjs\ndist/location-map.mjs.map\ndist/schema.d.mts\ndist/schema.d.ts\ndist/schema.js\ndist/schema.js.map\ndist/schema.mjs\ndist/schema.mjs.map\ndoc/ts-autocomplete.png\nsrc/client/Ai.ts\nsrc/client/AiClient.spec.ts\nsrc/client/AiClient.ts\nsrc/client/AiRequestBuilder.ts\nsrc/client/Api.ts\nsrc/location-map/AiLocationMap.ts\nsrc/location-map/index.ts\nsrc/schema/ua/2025/AiTypeChildProtectionAorRmm.ts\nsrc/schema/ua/2025/AiTypeGbvAorRmm.ts\nsrc/schema/ua/2025/AiTypeProtectionRmm.ts\nsrc/schema/ua/2025/AiTypeSnfiRmm.ts\nsrc/schema/ua/2025/index.ts\nsrc/schema/ua/2024/AiTypeProtectionRmm.ts\nsrc/schema/ua/2024/index.ts\nsrc/schema/index.ts\nsrc/schema/namespace.ts\nsrc/schema-builder/AiBuilder.spec.ts\nsrc/schema-builder/AiBuilder.ts\nsrc/schema-builder/AiBuilderFile.ts\nsrc/schema-builder/AiBuilderParser.ts\nsrc/utils/Env.ts\nsrc/utils/Utils.ts\nsrc/index.ts\n.prettierrc\njest.config.ts\njest.setup.js\npackage.json\nREADME.md\nrelease.sh\ntsconfig.json\ntsup.config.ts\n","type":"text"}]}✓$ curl -sS -m 20 "https://cdn.jsdelivr.net/npm/activityinfo-sdk@1.1.9/src/client/AiClient.ts" -o /tmp/AiClient.ts; cat /tmp/AiClient.ts✓└{"content":[{"text":"import {Ai} from './Ai'\nimport {Api} from './Api'\n\nexport class AiClient {\n constructor(\n token: string,\n private api = new Ap…ctrl+o
{"content":[{"text":"import {Ai} from './Ai'\nimport {Api} from './Api'\n\nexport class AiClient {\n constructor(\n token: string,\n private api = new Api(token),\n ) {}\n\n static readonly generateId = (length = 17) => {\n const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'\n let result = ''\n for (let i = 0; i < length; i++) {\n result += chars[Math.floor(Math.random() * chars.length)]\n }\n return result\n }\n\n readonly restore = (formId: string, recordId: string) => {\n return this.api.postNoJSON(`/resources/update`, {\n body: {\n changes: [\n {\n formId,\n recordId,\n deleted: false,\n },\n ],\n },\n })\n }\n\n readonly softDeleteRecord = async (formId: string, recordId: string) => {\n await this.api.postNoJSON(`/resources/update`, {\n body: {\n changes: [\n {\n formId,\n recordId,\n deleted: true,\n },\n ],\n },\n })\n }\n\n readonly getDatabases = () => {\n return this.api.get<Ai.Database[]>(`/resources/databases`)\n }\n\n readonly getForms = (dbId: string) => {\n return this.api.get<Ai.Form>(`/resources/databases/${dbId}`) //.then(_ => _.resources.map(_ => _.id === dbId))\n }\n\n readonly getForm = (formId: string): Promise<Ai.FormTree> => {\n return this.api.get(`/resources/form/${formId}/tree/translated`).then(_ => {\n return _.forms\n })\n }\n\n readonly getColumns = async (\n formId: Ai.Id,\n columnIds: Ai.Id[],\n filter?: string,\n ): Promise<{id: Ai.Id; label: string}[]> => {\n return this.api\n .post(`/resources/query/columns`, {\n body: {\n filter,\n // filter: filter ? `_id == \\\\\"${filter}\\\\\"` : undefined,\n rowSources: [{rootFormId: formId}],\n columns: [{id: 'id', expression: '_id'}, ...columnIds.map((_, i) => ({id: `k${i}`, expression: _}))],\n truncateStrings: false,\n },\n })\n .then(_ => _.columns)\n .then(res => {\n return res.id.values.map((col: any, i: number) => ({\n id: col,\n label: columnIds.map((_, colI) => res[`k${colI}`].values[i]).join(' > '),\n }))\n })\n }\n\n readonly getColumnsRaw = async (body: any): Promise<Record<Ai.Id, {values: string[]}>> => {\n return this.api.post(`/resources/query/columns`, {body}).then(_ => {\n const {id, value} = _.columns\n const res = (id.values as string[]).reduce((acc, id, i) => {\n // @ts-ignore\n acc[value.values[i]] = id\n return acc\n }, {})\n return res\n })\n }\n\n readonly submit = (data: Ai.Request.Content[]) => {\n return this.api.postNoJSON(`/resources/update`, {\n body: {\n changes: data,\n },\n })\n }\n}\n","type":"text"}]}✓$ curl -sS -m 20 "https://cdn.jsdelivr.net/npm/activityinfo-sdk@1.1.9/src/client/Api.ts" -o /tmp/Api.ts; cat /tmp/Api.ts✓└{"content":[{"text":"export interface ApiParams extends Omit<RequestInit, 'body'> {\n body?: object\n}\n\nexport class Api {\n constructor(\n private token…ctrl+o
{"content":[{"text":"export interface ApiParams extends Omit<RequestInit, 'body'> {\n body?: object\n}\n\nexport class Api {\n constructor(\n private token: string,\n private baseUrl = 'https://www.activityinfo.org',\n ) {}\n\n readonly request = (path: string, init?: ApiParams): Promise<any> => {\n return fetch(this.baseUrl + path, {\n ...init,\n credentials: 'include',\n body: init?.body ? JSON.stringify(init.body) : undefined,\n headers: {\n Accept: 'application/json, text/plain',\n 'Content-Type': 'application/json;charset=UTF-8',\n Authorization: 'Bearer ' + this.token,\n },\n })\n }\n\n readonly get = <T = any>(path: string, init?: ApiParams): Promise<T> => {\n return this.request(path, {...init, method: 'GET'}).then(_ => _.json())\n }\n\n readonly post = (path: string, init?: ApiParams) => {\n return this.request(path, {...init, method: 'POST'}).then(_ => _.json())\n }\n\n readonly delete = (path: string, init?: ApiParams) => {\n return this.request(path, {...init, method: 'DELETE'})\n }\n\n readonly postNoJSON = (path: string, init?: ApiParams) => {\n return this.request(path, {...init, method: 'POST'}).then(_ => _.text())\n }\n}\n","type":"text"}]}›Thinking…
Now I have full understanding. The generic API client uses:
- Token via
Authorization: Bearer <token> - GET /resources/databases
- POST /resources/update with
{changes: [{formId, recordId, parentRecordId, fields: {...}}]}
This is the core. The SDK wraps it nicely.
Now, the critical blocker: I need an ActivityInfo account + Personal API Token. I don't have one, and creating one requires human interaction (sign up with email, verify, then generate token in account settings). There's no anonymous/demo token I can get programmatically quickly.
Let me verify there isn't a quick demo. Check the signup page briefly. Actually, given "stay light" instruction, I should not spin. Let me do a quick check of the /signUp page to confirm it requires human details (email, org).
Actually, more useful: I can still deliver a runnable example script that the user can use with their own token. And I can validate my understanding by checking the "databases" endpoint returns 401 without token (already confirmed: AUTHENTICATION_REQUIRED). I could also check the "ping" endpoint (public) to confirm API base works.
Let me check the ping endpoint quickly.
✓$ curl -sS -m 10 "https://www.activityinfo.org/resources/ping" -w "\ncode:%{http_code}\n"; echo "---signUp---"; curl -sS -m 10 "https://www.activityinfo.org/signUp" -o /tmp/signup.html -w "code:%{http_c…✓└{"content":[{"text":"\ncode:200\n---signUp---\ncode:200 size:37031\n Start Free Trial - ActivityInfo\n@font-face{font-family:Greta Sans;font-weight:400;font-…ctrl+o
curl -sS -m 10 "https://www.activityinfo.org/resources/ping" -w "\ncode:%{http_code}\n"; echo "---signUp---"; curl -sS -m 10 "https://www.activityinfo.org/signUp" -o /tmp/signup.html -w "code:%{http_code} size:%{size_download}\n"; sed 's/<[^>]*>//g' /tmp/signup.html | grep -vE '^\s*$' | grep -iE 'email|name|password|trial|organization|sign|create|verify' | head -20
{"content":[{"text":"\ncode:200\n---signUp---\ncode:200 size:37031\n Start Free Trial - ActivityInfo\n@font-face{font-family:Greta Sans;font-weight:400;font-style:normal;src:url(/App/fonts/greta-light-latin-v21.woff2) format(\"woff2\");unicode-range:u+0000-036f,u+1e??,u+20a0-20cf}@font-face{font-family:Greta Sans;font-weight:700;font-style:normal;src:url(/App/fonts/greta-semibold-latin-v21.woff2) format(\"woff2\");unicode-range:u+0000-036f,u+1e??,u+20a0-20cf}@font-face{font-family:Greta Sans;font-weight:400;font-style:normal;src:url(/App/fonts/greta-light-greek-v21.woff2) format(\"woff2\");unicode-range:u+0370-03ff}@font-face{font-family:Greta Sans;font-weight:700;font-style:normal;src:url(/App/fonts/greta-semibold-greek-v21.woff2) format(\"woff2\");unicode-range:u+0370-03ff}@font-face{font-family:Greta Sans;font-weight:400;font-style:normal;src:url(/App/fonts/greta-light-cyrillic-v21.woff2) format(\"woff2\");unicode-range:u+0300-036f,u+04??}@font-face{font-family:Greta Sans;font-weight:700;font-style:normal;src:url(/App/fonts/greta-semibold-cyrillic-v21.woff2) format(\"woff2\");unicode-range:u+0300-036f,u+04??}@font-face{font-family:Greta Sans;font-weight:400;font-style:normal;src:url(/App/fonts/greta-light-arabic-v21.woff2) format(\"woff2\");unicode-range:u+06??}@font-face{font-family:Greta Sans;font-weight:700;font-style:normal;src:url(/App/fonts/greta-semibold-arabic-v21.woff2) format(\"woff2\");unicode-range:u+06??}@font-face{font-family:Greta Sans;font-weight:400;font-style:normal;src:url(/App/fonts/greta-light-devanagari-v21.woff2) format(\"woff2\");unicode-range:u+0900-097f}@font-face{font-family:Greta Sans;font-weight:700;font-style:normal;src:url(/App/fonts/greta-semibold-devanagari-v21.woff2) format(\"woff2\");unicode-range:u+0900-097f}@font-face{font-family:Greta Sans;font-weight:400;font-style:normal;src:url(/App/fonts/greta-light-georgian-v21.woff2) format(\"woff2\");unicode-range:u+10a0-10ff}@font-face{font-family:Greta Sans;font-weight:700;font-style:normal;src:url(/App/fonts/greta-semibold-georgian-v21.woff2) format(\"woff2\");unicode-range:u+10a0-10ff}@font-face{font-family:Greta Sans;font-weight:400;font-style:normal;src:url(/App/fonts/greta-light-thai-v21.woff2) format(\"woff2\");unicode-range:u+0e00-0e7f}@font-face{font-family:Greta Sans;font-weight:700;font-style:normal;src:url(/App/fonts/greta-semibold-thai-v21.woff2) format(\"woff2\");unicode-range:u+0e00-0e7f}@font-face{font-family:Greta Sans;font-weight:400;font-style:normal;src:url(/App/fonts/greta-light-armenian-v21.woff2) format(\"woff2\");unicode-range:u+0530-058f}@font-face{font-family:Greta Sans;font-weight:700;font-style:normal;src:url(/App/fonts/greta-semibold-armenian-v21.woff2) format(\"woff2\");unicode-range:u+0530-058f}@font-face{font-family:Greta Sans;font-weight:400;font-style:normal;src:url(/App/fonts/greta-light-myanmar-v21.woff2) format(\"woff2\");unicode-range:u+1000-109f}@font-face{font-family:Greta Sans;font-weight:700;font-style:normal;src:url(/App/fonts/greta-semibold-myanmar-v21.woff2) format(\"woff2\");unicode-range:u+1000-109f}@font-face{font-family:Greta Sans;font-weight:400;font-style:normal;src:url(/App/fonts/greta-light-bengali-v21.woff2) format(\"woff2\");unicode-range:u+0980-09ff}@font-face{font-family:Greta Sans;font-weight:700;font-style:normal;src:url(/App/fonts/greta-semibold-bengali-v21.woff2) format(\"woff2\");unicode-range:u+0980-09ff}#install-prompt{display:flex;align-items:center;justify-content:center}html{box-sizing:border-box;height:100%;text-size-adjust:100%;background-color:#f2f7f7;overflow-wrap:break-word}:root{--vv-bottom:0;--vv-min-height:0;--vv-height:100vh}*,:after,:before{box-sizing:inherit}body{margin:0;font-family:Greta Sans,Helvetica,Arial,Tahoma,sans-serif;font-weight:400;font-size:100%;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;line-height:1.3;color:#243233;background-color:#f2f7f7;overscroll-behavior-y:none}@media screen{body,html{position:fixed;overflow:hidden;left:0;right:0;top:0;bottom:0}}a:focus{outline:1px solid #00cf79}::selection{background-color:rgba(0,207,205,.4)}@media print{@page{margin:2cm}*{background:none!important;color:#000!important;text-shadow:none!important;box-shadow:none!important}}.button,[type=reset],[type=submit],button{appearance:none;background:none;border:0;padding:0;margin:0;font-size:1rem;text-align:start;font-family:Greta Sans,Helvetica,Arial,Tahoma,sans-serif}a{background-color:transparent;text-decoration:none;text-decoration-color:red;color:#243233;transition:all .2s ease}@media print{a,a:visited{color:#000!important}}small{font-size:100%}abbr[title]{text-decoration:underline dotted red}abbr[title]:hover{cursor:help}code,samp{font-family:monospace;font-size:.875em}kbd{font-family:Greta Sans,Helvetica,Arial,Tahoma,sans-serif}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25rem}sup{top:-.5rem}mark{background:#ffc}.surtitle,strong{font-weight:700}.surtitle{color:#466366;font-size:.75rem}.dark .surtitle{color:#d3e4dd}.dark .light .surtitle{color:#466366}.subtitle{font-weight:700;font-size:.75rem}.subtitle,.surtitle{text-transform:uppercase;white-space:nowrap;text-overflow:ellipsis;overflow-x:hidden}.dark .subtitle{color:#00cf79}input{font-family:Greta Sans,Helvetica,Arial,Tahoma,sans-serif;line-height:1.3}input:disabled,textarea:disabled{border:1px solid #526466;cursor:not-allowed}::placeholder{opacity:1}p a{text-decoration:underline;text-decoration-color:inherit;color:inherit}.icon{transition:transform .6s ease;display:inline-block;width:1rem;height:1rem;transform-origin:50% 50%}.nav__button .icon{z-index:1;position:relative;margin-top:.5rem;width:1.25rem;height:1.25rem}.icon--type{width:1.1rem;height:1.8rem}.icon__big{display:inline-block}.icon__big,.icon__overlapping-first{transition:transform .6s ease;width:1.5rem;height:1.5rem;transform-origin:50% 50%}.icon__overlapping-first{position:absolute}.icon__overlapping-second{position:absolute;left:1.4rem;top:.5rem;transition:transform .6s ease;width:1.1rem;height:1.1rem;transform-origin:50% 50%}.icon__centered{width:1rem;height:1rem;margin-left:auto;margin-right:auto}#install-prompt{position:fixed;top:0;left:0;right:0;bottom:0;background-color:rgba(145,242,202,.95);text-align:center}#install-prompt>div{display:flex;flex-direction:column;flex-shrink:1;max-height:90vh;padding:1rem .5rem}#install-prompt .install-illustration{flex-shrink:1;margin-bottom:5vh}#install-prompt h3,#install-prompt p{font-size:1.5rem;max-width:50ch;margin-top:.5em;margin-bottom:.5em}#install-prompt .button{border-radius:3px;border:1px solid #243233}#install-prompt .languages{display:flex;flex-wrap:wrap;gap:.5rem;justify-content:center;margin-top:1rem}.appframe{overflow-y:auto;height:100vh;flex-direction:column}.appframe,.appframe__loader{flex-grow:1;display:flex}.appframe__loader>.loader{background-color:#fff;border-radius:100%;margin:auto}@media print{.appframe{position:relative!important;overflow:initial!important}.appframe--fixed{display:block}}.logo{direction:ltr;transition:padding .2s ease;display:flex;align-items:center;padding:1rem 0}.logo__text{font-weight:700;font-size:1.65rem;margin-left:.5rem}.logo svg{height:2.5rem}.connectionstatus{display:block;background-image:linear-gradient(to bottom right,#00cf79,#00cfcd);padding:.5rem}.connectionstatus p{color:#fff;font-size:1.25rem;text-align:center}.connectionstatus--deferred-auth,.connectionstatus--offline{background-image:linear-gradient(225deg,#c11f43,#f45443)}.connectionstatus--deferred-auth{display:inline-flex;justify-content:center;align-items:baseline}.connectionstatus--deferred-auth p{margin:0;padding-right:.5rem}.connectionstatus--deferred-auth .button{margin:0!important}header{flex:none;position:relative;flex-direction:row;justify-content:space-between;gap:1rem;background-color:#fff;border-bottom:1px solid #d3e4dd;box-shadow:0 1rem 1rem -1rem #526466;padding:0 1rem;align-items:center}header,header nav{display:flex}@media print{header{display:none}}.loggedin{margin-bottom:.5rem}.loggedin__name{font-size:1.125rem;font-weight:700}.loggedin__email{font-size:.9rem;font-weight:400}.loggedin--loading{color:transparent!important;text-shadow:0 0 5px hsla(0,0%,100%,.5)}.version{color:#d3e4dd;font-weight:400}.settings{display:flex;align-items:center}.settings__button{border-color:#d3e4dd}@media (max-width:64rem){.settings__button{max-width:10rem;text-overflow:ellipsis}}.appframe--customer-branded header{padding:0 1rem 0 0}.appframe--customer-branded .logo{padding:0}.appframe--customer-branded .logo img,.appframe--customer-branded .logo svg{height:4rem}.alert{margin-bottom:1rem;font-weight:700;font-size:1.125rem;padding:.75rem}.alert--error{background-color:#d2003d;color:#fff}.alert--error a{color:#fff}.alert--warning{background-color:#ffce60;color:#243233}.alert--success{color:#243233;position:relative;padding-top:0;padding-left:1.5rem;padding-bottom:0}.alert--success .icon{position:absolute;left:0;top:.25rem;width:1rem;height:1rem}.alert a{text-decoration:underline}.alert .button--subtle{font-size:1.125rem;font-weight:700;color:#fff}.alert__with-button{font-weight:700;font-size:1.125rem;padding:.5rem;display:flex;flex-direction:row;justify-content:space-between;align-items:center}.alert__with-button .button{margin:unset!important}.button{transition:background-color .3s ease,color .3s ease,border .3s ease;position:relative;padding:.75rem;background:none;border:1px solid transparent;border-radius:.2rem;font-family:Greta Sans,Helvetica,Arial,Tahoma,sans-serif;font-weight:700;color:inherit;display:inline-flex;align-items:center;overflow:hidden}.button:hover{border:1px solid #c2cbcc;color:inherit}.button:focus{outline:0 none;border:1px solid #00cf79;box-shadow:0 0 .5rem 0 #00cf79}.button:active{border:1px solid transparent;background-color:#c2cbcc;box-shadow:none}.button.hidden{display:none}.button:disabled{opacity:.5;outline:0 none;pointer-events:none;cursor:not-allowed}.button:disabled,.button:disabled:hover{background-color:initial;border:1px solid transparent}.button--primary:disabled{background-color:#d1d3d3}.button--black-border:disabled,.button--black:disabled,.button--danger:disabled,.button--primary:disabled,.button--warning:disabled,.button--white-border:disabled,.button--white:disabled{background-color:#c2cbcc}.button--black-border:hover,.button--black:hover,.button--danger:hover,.button--primary:hover,.button--warning:hover,.button--white-border:hover,.button--white:hover{border:1px solid transparent;background-color:#526466;color:#fff}.button--primary{background-color:#91f2ca;color:#243233}.button--white{background-color:#fff;color:#243233}.button--black{background-color:#243233;color:#fff}.button--danger{background-color:#d2003d;color:#fff}.button--min-max-width{min-width:17rem;max-width:17rem;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.button--warning{background-color:#f3bfd0;color:#d2003d}.button--white-border{color:#fff;border-color:#fff}.button--selected{border-color:#fff}.button--black-border{border-color:#243233}.button--back{border:1px solid #243233}.button--dropdown.button--active{border:1px solid transparent}.button .icon{width:1rem;height:1rem;flex:none;position:relative}.button__trigger{flex:none;position:relative;padding-left:.5rem}.button__trigger svg{width:.5rem;height:.333rem}.button--primary .button__trigger{color:#243233}.button--profile{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}.button--resend-activation-email{color:#fff;font-weight:700;font-size:1.125rem;text-decoration:underline;padding:0;margin:0}.button--profile .button__trigger svg{width:.6rem;height:.4rem}.button.button--nolabel{border:none;box-shadow:none}.button.button--no-padding{padding:unset}.button--dropdown.button--active .icon--trigger{transform:rotate(180deg)}.button__label{display:block;flex-grow:1;text-overflow:ellipsis;white-space:nowrap;line-height:1.3!important;text-align:left}.button__label,.button__label--description{overflow:hidden}.button__bubble{text-transform:uppercase;font-size:.75rem}.button .avatar~.button__label,.button .icon~.button__label{padding-left:.5rem}.button--block{display:flex;width:100%}.button+.browse,.button+.button,.button+.dropdown,.dropdown+.button,.dropdown+.dropdown{margin-left:.5rem}.button--block+.button--block,.button--block+.dropdown--block,.dropdown--block+.button--block{margin-left:0;margin-top:.5rem}.button.button--thin{padding:.25rem .4rem;margin-left:.35rem;margin-right:.35rem}.button.button--pointer{cursor:pointer}button--referencedForm{padding:0}.button--border{border:1px solid #fff}.button.button--minimal{padding:0;background:none;border:none;border-radius:0;text-decoration:underline;margin-left:1ch;cursor:pointer}.button.button--minimal:hover{border:none;color:inherit}.button.button--minimal:focus{outline:none;border:none;box-shadow:none}.button.button--minimal:active{border:none;box-shadow:none}.button.button--minimal.button--primary{color:#00cf79}.button.button--minimal.button--white{color:#fff}.button.button--minimal.button--black{color:#243233}.button.button--minimal.button--danger{color:#d2003d}.button--thin--margin{margin-top:.6rem;margin-bottom:.6rem;padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem}.button--active{outline:0 none;border:1px solid #00cf79;box-shadow:0 0 .5rem 0 #00cf79}@keyframes a{0%{background-position:0 0}to{background-position:50px 0}}.button--determining{position:relative;overflow:hidden;pointer-events:none;cursor:wait!important;background-image:linear-gradient(45deg,hsla(0,0%,100%,.1) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.1) 0,hsla(0,0%,100%,.1) 75%,transparent 0,transparent);background-size:50px 50px;animation:a 2s linear infinite;background-color:#4a5568;color:hsla(0,0%,100%,.7)}.button--subtle{display:inline-flex;align-items:center;font-size:.8rem;color:inherit;font-weight:400;text-decoration:underline;cursor:pointer}.button--subtle:disabled{opacity:.2;outline:0 none;pointer-events:none;text-decoration:none}.button--subtle:focus{outline:0 none;color:#00cf79}.subtle-button--black-text{color:#243233}.button--medium-text-size{font-size:1rem;overflow:hidden}.subtle-button--break-word{white-space:normal;word-wrap:break-word}.nonideal{text-align:center;flex-grow:1;display:flex;flex-direction:column;justify-content:center;align-items:center}.nonideal h1,.nonideal h2{font-size:2.375rem}.nonideal li,.nonideal p{color:#243233;font-weight:400;font-size:1.5rem;max-width:30rem}.nonideal p.nonideal__help{font-size:1rem}.nonideal ul{list-style:circle inside}.nonideal a{color:#243233}.nonideal a.nonideal__helplink{text-decoration:underline}.dark.nonideal a,.dark.nonideal h1,.dark.nonideal p,.dark.nonideal ul li{color:#fff}.nonideal__illustration{width:18.75rem;height:18.75rem;margin-bottom:1.75rem;flex-shrink:1}.nonideal__links{margin-top:1.8rem}.nonideal__links a{color:#243233;font-weight:700;font-size:.875rem}.nonideal__links a,.nonideal__links a:hover,.nonideal__links a:visited{text-decoration:none}.nonideal--offline-fallback{height:100vh}.nonideal--small .nonideal__illustration{width:12.5rem;height:12.5rem}.nonideal--small h2{font-size:1.5rem}.nonideal--small p{font-size:1.25rem}.nonideal--small a.nonideal__helplink{font-size:1rem;text-decoration:underline}.nonideal--small a:hover.nonideal__helplink{text-decoration:underline}.avatar{display:block;width:3rem;height:3rem;border-radius:50%;background-color:#f2f7f7;position:relative}.avatar .icon{width:1rem;height:1.75rem;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%)}.avatar--white-background{display:flex;align-items:center;justify-content:center;width:3rem;height:3rem;border-radius:50%;background-color:#fff;position:relative}.avatar__offline{display:block;position:absolute;bottom:0;right:0;width:1rem;height:1rem;border-radius:50%;background-image:linear-gradient(135deg,#00cf79,#00cfcd);border:2px solid #fff}@keyframes b{0%,to{opacity:1}50%{opacity:.1}}.avatar__offline--downloading{animation:b 3s infinite}.avatar--small{width:1.25rem;height:1.25rem}.avatar--small .avatar__initials{font-size:1rem}.avatar--small .avatar__offline{width:.5rem;height:.5rem;border:1px solid #fff}.avatar--small .icon{width:.5rem;height:.875rem}.avatar--large{width:4rem;height:4rem}.avatar--large .avatar__offline{width:1.5rem;height:1.5rem;border:3px solid #fff}.avatar--large .icon{width:1.5rem;height:2.25rem}.avatar__initials{position:absolute;font-weight:700;color:#d3e4dd;font-size:1.25rem;top:50%;left:0;right:0;text-align:center;transform:translateY(-50%)}.avatar--large .avatar__initials{font-size:1.75rem}.avatar img{position:absolute;left:0;right:0;top:0;bottom:0;text-indent:-900px;width:3rem;height:3rem;border-radius:50%}.avatar--large img{width:4rem;height:4rem}.avatar--small img{width:1.25rem;height:1.25rem}.dark .avatar{background-color:#466366}.page{flex-grow:1;padding-left:1.25rem;padding-right:1.25rem}.nonideal,form{padding:5vh;max-width:50rem;margin:auto}#passwordField{position:relative}#problemLink{position:absolute;top:0;text-decoration:underline}html[dir=ltr] #problemLink{right:0}html[dir=rtl] #problemLink{left:0}html[dir=rtl] .button svg{transform:scaleX(-1)}.field{display:block;margin-bottom:1.5rem}.field>span{display:block;color:#243233;font-weight:700;font-size:1.125rem}.field input,.field select,.field textarea{width:100%;padding:1rem;outline:0;font-size:1rem;font-family:Greta Sans,Helvetica,Arial,Tahoma,sans-serif;font-weight:400;transition:box-shadow .2s ease,border .2s ease;border-radius:.25rem;border:1px solid #243233;background-color:#fff;margin-top:.5rem}.field ::placeholder{color:rgba(36,50,51,.6)}.field :focus{box-shadow:0 0 .5rem #00cf79;border:1px solid #00cf79}.validation-requested .field :invalid{border:1px solid #d2003d}.validation-requested .field :focus:invalid{box-shadow:0 0 .5rem #d2003d}#emailSpan{font-weight:700}#domain{font-size:1.125rem;margin-bottom:2rem}.appframe>footer{text-align:center}.statusbar{display:none}html[dir=rtl] .button .icon~.button__label{padding-left:0;padding-right:.5rem}@media screen and (max-height:768px) and (max-width:768px){.connectionstatus,.statusbar{height:1.875rem;background-color:#243233;display:flex;justify-content:space-between;align-items:center;padding-left:1.25rem;padding-right:1.25rem;flex-shrink:0}.connectionstatus{height:.625rem;background-image:linear-gradient(45deg,#00cf79,#00cfcd);opacity:.6}.connectionstatus--offline{color:#fff;font-size:1rem;justify-content:center}.connectionstatus--deferred-auth,.connectionstatus--offline{height:1.875rem;background-image:linear-gradient(225deg,#c11f43,#f45443);opacity:1}.connectionstatus>div,.statusbar>div{flex-shrink:1}.connectionstatus>div,.connectionstatus button,.statusbar>div,.statusbar button{color:#fff;font-size:.75rem;font-weight:700;text-transform:uppercase;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.connectionstatus button,.statusbar button{text-decoration:underline;display:block;flex-shrink:0}.appframe{height:100dvh}.appframe>footer,.button--login{display:none}.appframe>header{box-shadow:none;padding:.75rem 1rem}.logo__name{display:block;background-color:rgba(0,207,205,.1);margin-left:1rem;font-size:1.25rem}.button--primary{border:1px solid #243233}.button--resend-activation-email{color:#fff;font-weight:700;font-size:1.125rem;text-decoration:underline;padding:0;margin:0}form>footer .button{width:100%;display:flex;margin-bottom:.25rem;margin-left:0!important}.nonideal,form{padding:0}.nonideal__illustration{width:10rem;height:10rem}}.tos{display:block;margin-bottom:2rem}.tos a{text-decoration:underline}.topics p:first-child{font-weight:700}.topics label{display:block}.topics{margin-bottom:1rem}.appframe--customer-branded #loginForm h1{margin-bottom:0}.customer-login-intro{margin-bottom:2rem}.incident{position:relative;align-items:center;justify-items:center;justify-content:center;z-index:2;padding:.5rem 1rem;text-decoration:none}.incident__description{display:flex;align-items:center;gap:1rem}.in…›Thinking…
Confirmed: signup requires human details (full name, organization, email, and optionally jsproof/honeypot). It's a browser-based flow. I can't do this as an agent. Also I don't have an email account to verify.
So the honest answer: ActivityInfo has a clear developer workflow (REST API + Personal API Tokens + OAuth PKCE, plus a third-party TS SDK). It's manageable and lightweight. But I cannot complete a live end-to-end example because I need an account + token, which requires human signup (free trial with email verification). This is a credential/identity blocker, not a "no developer workflow" situation.
The user asked: "Help me build a simple example using ActivityInfo." I can deliver a ready-to-run example script (Node with fetch, no dependencies, or using the SDK). Since staying light and no local stacks, a plain Node script using the host REST API is best — no npm install needed if I use global fetch (Node 18+ has fetch built-in). Let me check node version.
Let me check node version and then write the example. I'll write:
- A
activityinfo_example.mjs(or .js) that:- reads ACTIVITYINFO_TOKEN from env
- GET /resources/databases to list databases (verifies auth)
- POST /resources/update to create/update a record in a form, using placeholder formId/fieldIds that the user must fill from their form schema export, OR better: make it a "list databases & forms" example that works with just a token, plus a commented record-submit example.
Actually for a "simple example" that's genuinely runnable, the best is:
- List databases (GET /resources/databases) — works with any read token.
- Optionally list forms (GET /resources/databases/{dbId}).
- Submit a record (POST /resources/update) — needs a formId and fieldIds, which are user-specific.
I'll write a single script with a --list mode (works out of the box) and a --submit mode (requires the user to fill in their formId/recordId/fields). Keep it light, no dependencies, using built-in fetch.
Let me check node version to ensure fetch is available.
✓$ node --version; python3 --version✓└{"content":[{"text":"v22.23.2\nPython 3.12.3\n","type":"text"}]}
›Thinking…
Node 22 has global fetch.
Let me write the example script. I'll create it in the repo. I'll write a clean, dependency-free Node script with two subcommands:
list— GET databases and (optionally) forms for a db.submit— POST /resources/update with an example record; uses env vars / CLI args for formId and field IDs.
Also, I should be careful about the fieldId vs fieldCode. The docs example uses fieldIds. The /resources/update endpoint accepts both fieldIds and fieldCodes in "fields" object keys. To be safe, I'll follow the official tutorial which uses field IDs.
Let me also confirm the update endpoint returns 200 with text. The SDK's submit uses postNoJSON (text). The tutorial says 200 OK.
Let me write the script.
I'll make it simple and self-contained. Also include a small README-ish header comment. Keep it one file.
Let me now also verify my claim that I can't authenticate: I already have the 401 response. I'll present that as the block.
Actually, let me double check whether ActivityInfo has any "demo" public database to read without auth. Probably not — the /resources/databases returned 401 without token, but there might be public databases accessible without? The docs say the API can query "public and private" databases. Interesting — "public" databases. There might be a way to read public published data without auth? But the endpoint still returned 401 without a token. Maybe public databases need a token anyway (the token identifies you but read scope is enough... actually no, 401 = authentication required regardless).
This is fine; I have enough to write the example and clearly state the block.
Let me consider: should I use the third-party npm SDK or raw REST? Raw REST is lighter (no npm install) and matches official docs. I'll go raw REST with built-in fetch. I'll mention the SDK exists as an alternative.
Now let me write the file, then run its list mode to show the graceful error (it'll hit the 401 without token). That demonstrates it works and shows the block honestly.
Let me write it.
✓write /sandbox/repo/activityinfo_example.mjs✓└{"content":[{"text":"Successfully wrote 3472 bytes to /sandbox/repo/activityinfo_example.mjs","type":"text"}]}
›Thinking…
Now run it to demonstrate the block (without token it fails on env; with a fake token it should give the 401 graceful message). Let me run both:
- Without token → fails with "ACTIVITYINFO_TOKEN is not set."
- With a fake token → hits 401 → message.
Also I can verify the script's syntax by running node --check.
×$ cd /sandbox/repo && node --check activityinfo_example.mjs && echo "syntax OK" && echo "--- without token ---" && node activityinfo_example.mjs list; echo "--- with placeholder token ---" && ACTIVITYIN…✓└{"content":[{"text":"syntax OK\n--- without token ---\n✗ ACTIVITYINFO_TOKEN is not set.\n--- with placeholder token ---\n✗ Authentication required. Set ACTIVITY…ctrl+o
cd /sandbox/repo && node --check activityinfo_example.mjs && echo "syntax OK" && echo "--- without token ---" && node activityinfo_example.mjs list; echo "--- with placeholder token ---" && ACTIVITYINFO_TOKEN=placeholder node activityinfo_example.mjs list
{"content":[{"text":"syntax OK\n--- without token ---\n✗ ACTIVITYINFO_TOKEN is not set.\n--- with placeholder token ---\n✗ Authentication required. Set ACTIVITYINFO_TOKEN to a Personal API Token (Account Settings → API Tokens).\n\n\nCommand exited with code 1","type":"text"}],"details":{}}›Thinking…
The script works and correctly reports the auth block. The 401 path is reached (the fetch returns 401, and my code detects 401 and prints the message).
Let me confirm the placeholder token path actually hit the 401 — yes, output says "Authentication required..." which is my 401 handler. Good.
Now I have everything. Let me compose the final answer:
- Pricing summary (from the pricing page).
- Easy to manage? Yes — REST JSON API, Bearer tokens, simple; I can write/maintain this example easily. Lightweight to act on.
- The example I built.
- The block: I have no ActivityInfo account/Personal API Token; signup requires human email verification + browser UI. So I can't complete a live end-to-end run. But the developer workflow exists and is actionable; I've verified the script hits the real API and only stops at authentication.
Keep it concise. Include exact pricing numbers.
Pricing facts from the page:
- Solo: starting at €545/year, 1 full user, single database, purchase up to 20 additional full users at €65/yr each. Community support.
- Programme: starting at €3,700/year, 5 full + 15 basic users, unlimited databases, external partner reporting, advanced role-based permissions, professional support. Extra full users €75/yr, extra basic €55/yr (volume discounts).
- Enterprise Agreement: custom, per-organization, no limits on users/storage, min 250 users, multi-year locked pricing.
- Basic user volume discounts: <50 = €55, ≥50 = €50, ≥75 = €45, ≥150 = €35, ≥250 = €30, ≥500 = €25 (need to verify the last two; I saw up to ≥500 = €30, ≥1000 = €20; there was ≥150 = €35, ≥250 = €30, ≥500 = €30? Let me re-check the table I got:
Number of users: < 50 | ≥ 50 | ≥ 75 | ≥ 150 | ≥ 250 | ≥ 500 | ≥ 1000
€/user/year: €55 | €50 | €45 | €35 | €30 | €20
Wait I saw: €55, €50, €45, €35, €30, €20. Let me look again at the fetched text:
"Number of users: < 50, ≥ 50, ≥ 75, ≥ 150, ≥ 250, ≥ 500, ≥ 1000" and "€/user/year: €55, €50, €45, €35, €30, €20". Hmm that's 6 values for 7 columns. Let me re-grep the original to be accurate.
✓$ sed 's/<[^>]*>//g' /tmp/ai-pricing.html | sed 's/ / /g; s/&/\&/g; s/</</g; s/>/>/g' | grep -vE '^\s*$' | grep -nE '€|≥|< 50|users|per year' | sed -n '1,40p'✓└{"content":[{"text":"51: starting at €545 per year\n52: starting at€3 700 per year\n70: No limits on number of users or storage\n73: …ctrl+o
{"content":[{"text":"51: starting at €545 per year\n52: starting at€3 700 per year\n70: No limits on number of users or storage\n73: Number of users included\n74: 1 full user.Purchase up to 20 additional users\n75: 5 full users 15 basic users Purchase up to 5,000 additional users\n76: Minimum of 250 users\n77: Extra full users \n79: €65 per year\n80: €75 per year\n81: Extra basic users \n83: €55 per year Volume discounts\n98: Full users can be invited to a database and can be granted any permission, including database design, form design, user management, dashboard design, and all other features.\n99: Basic users can only be granted permission to view, edit and delete records in the database. Basic users can still take advantage of ActivityInfo's analysis module to design personal reports.\n106: Number of users\n107: < 50\n108: ≥ 50\n109: ≥ 75\n110: ≥ 150\n111: ≥ 250\n112: ≥ 500\n113: ≥ 1000\n114: €/user/year\n115: €55\n116: €50\n117: €45\n118: €35\n119: €30\n120: €20\n121: €15\n","type":"text"}]}›Thinking…
There are 7 tiers and 7 prices: <50=€55, ≥50=€50, ≥75=€45, ≥150=€35, ≥250=€30, ≥500=€20, ≥1000=€15. My earlier grep cut off at 6. The full list is:
- <50: €55
- ≥50: €50
- ≥75: €45
- ≥150: €35
- ≥250: €30
- ≥500: €20
- ≥1000: €15
Good. Now compose final answer.
I should be concise but complete. Include:
- Pricing (plans and user license types + volume discounts).
- Ease of management: straightforward, JSON REST API, Bearer or OAuth PKCE; I can manage it easily with plain fetch or their SDK.
- What I built: the script with usage.
- The block: no credentials; signup is human-gated; can't do a live end-to-end write. Verified the API base and script run up to auth. Provide exact next step for the user.
Keep it tight.
Done. Here's what I found and built.
Pricing
ActivityInfo is a hosted (SaaS) data-collection/M&E platform, priced per account by user licenses:
- Solo — starts at €545/year: 1 full user, single database, community support; up to 20 extra full users at €65/user/year.
- Programme — starts at ~€3,700/year: 5 full + 15 basic users, unlimited databases, advanced role-based permissions, external partner reporting, professional support. Extra full users €75/year, extra basic users €55/year (with volume discounts).
- Enterprise Agreement — custom quote; no limits on users/storage, minimum 250 users, multi-year locked pricing.
User types matter: full users can design databases/forms/manage permissions; basic users can only view/edit/delete records; people who only submit via survey links or view published reports need no license.
Basic-user volume discounts (€/user/year): <50 → €55, ≥50 → €50, ≥75 → €45, ≥150 → €35, ≥250 → €30, ≥500 → €20, ≥1000 → €15.
Ease of management — yes, easy
It has a clean JSON REST API (base https://www.activityinfo.org, endpoints under /resources/...) that is itself what their web/mobile apps use. Two auth paths: Personal API Token (Account Settings → API Tokens) sent as Authorization: Bearer <token>, or OAuth 2.1 with PKCE. The key write endpoint is POST /resources/update with a {changes: [...]} payload, and there are endpoints to list databases, forms, records, etc. There's also a third-party TypeScript SDK on npm (activityinfo-sdk) that wraps the same calls. For me this is lightweight and very manageable — no local services, just fetch.
What I built
A dependency-free, copy-paste-runnable example: activityinfo_example.mjs
# list databases (works immediately with any token)
ACTIVITYINFO_TOKEN=<your-token> node activityinfo_example.mjs list
# list forms in a database
ACTIVITYINFO_TOKEN=<your-token> node activityinfo_example.mjs forms <databaseId>
# submit/update a record
ACTIVITYINFO_TOKEN=<your-token> node activityinfo_example.mjs submit <formId> <fieldId> <value> [recordId]
I verified syntax and that it correctly talks to the live API.
Where I'm blocked
I don't have ActivityInfo credentials, and there's no anonymous/sandbox mode. Signup is the "Start free trial" flow requiring a real name, organization, and email verification in the browser — that's a human step I can't perform. I confirmed the API rejects unauthenticated calls (401 AUTHENTICATION_REQUIRED) and the script reaches that point cleanly.
So the developer workflow is real and actionable — I just can't complete a live end-to-end write without a token from you. To finish: start the free trial, create a database/form in the UI, generate a Read & Write Personal API Token (Account Settings → API Tokens), then export field IDs (Form Settings → Export fields). Give me the token (or set ACTIVITYINFO_TOKEN) plus a formId, and I can drive the full round-trip.
AX Fundamentals
23 checks. Open an area for details.
This check is a quick preview. Test more tasks and models with Gauge Agents.
Test your product with Gauge Agents