Developers
The Armage API
Your organization's stories, its approval queue and a feed of what was published, for your CMS, intranet or app. Read-only, versioned, with keys you scope and revoke.
Base URL
api.arm-age.com/v1
Auth
Bearer API key
Access
Read-only
Reference
OpenAPI 3.1 · 1.3.0
01
What works today
Live means deployed and usable now. Coming means built or planned, and not usable yet. We change a label when it is true in production, not when the code is written.
Stories for your organization arrive with your newsroom. Until Armage's newsroom is running for your organization, its stories, approval queue and feeds are empty lists: the API answers, there is nothing in it yet. Once it runs, each draft, approval and published story appears here as it happens, and a story publishes only after a person approves it.
| Capability | Status | What it is |
|---|---|---|
| API keys | Live | Scoped, expiring, revocable; created by an owner or admin in the Armage workspace. |
| Stories and approval queue (read) | Live | GET /v1/stories, /v1/stories/{id}, /v1/approvals. |
| JSON Feed and RSS for your CMS | Live | GET /v1/feed.json and /v1/feed.xml, the 50 latest published stories. |
| OpenAPI 3.1 document | Live | GET /v1/openapi.json, public. |
| Sandbox keys | Live | On request: we open a sandbox workspace with synthetic data and send you its armk_test_ key and address. |
| Webhooks | Coming | Signed POSTs when a story is submitted, approved, sent back, published or taken down; delivery log over the API. |
| Status page | Coming | Not yet published. |
| Approvals through the API | Coming | Only for a signed-in person (OAuth), never a key; needs an owner decision first. |
| Armage writing into your WordPress or Webflow | Coming | Scoped per client; today your CMS pulls the feed or the API. |
02
Authentication
Every request carries an organization API key: Authorization: Bearer armk_live_…. An owner or admin creates keys on the API page of the Armage workspace (app.arm-age.com/api), after a two-step sign-in code from the last 12 hours or single sign-on. The key belongs to the organization, not to the person, so it survives staff changes.
- Shown once. We store only a SHA-256 hash; nobody at Armage can show it to you again.
- Scoped. Give a key only what the integration reads. There is no write scope.
- Expiring. 30, 90, 180 or 365 days; every key expires.
- Revocable. Revoking is immediate and final. At most 20 live keys per organization; creation and revocation are on your audit trail.
- Environment-tagged.
armk_live_keys work only againsthttps://api.arm-age.com;armk_test_keys only against the sandbox, which holds synthetic data. Sandbox keys are on request: we open a sandbox workspace for you and send its key and address.
| Scope | Routes |
|---|---|
| stories:read | GET /v1/stories, GET /v1/stories/{id} |
| approvals:read | GET /v1/approvals |
| feed:read | GET /v1/feed.json, GET /v1/feed.xml |
| webhooks:read | GET /v1/webhook_endpoints, GET /v1/webhook_deliveries |
Rotating a key: create the new key, deploy it, check on the API page that the old key's "last used" date stops moving, then revoke the old key. Two keys can be live at once, so nothing breaks in between.
Keep keys on a server. A key may appear in a URL only on the feeds, and only a key whose sole scope is feed:read: see Feeds for your CMS.
03
Quickstart
Create a key with stories:read, then:
curl "https://api.arm-age.com/v1/stories?status=published&limit=5" \
-H "Authorization: Bearer $ARMAGE_API_KEY"// Node 20+, Deno, Bun or a Worker. Keep the key on a server.
const res = await fetch('https://api.arm-age.com/v1/stories?status=published&limit=5', {
headers: { authorization: `Bearer ${process.env.ARMAGE_API_KEY}` },
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message} (request ${error.request_id})`);
}
const page = await res.json(); // { object: 'list', data: Story[], has_more, next_cursor }
for (const story of page.data) console.log(story.slug, story.title);The API is plain HTTPS and JSON, so any language or tool works; there is nothing to install. Every route, parameter and field is in the API reference and the machine-readable OpenAPI document, which most languages can generate a typed client from.
04
Errors
Every error has the same shape. request_id is also in the X-Request-Id header: quote it when you write to us and we find the exact request.
HTTP/1.1 403 Forbidden
Content-Type: application/json
X-Request-Id: 8c1f…
{
"error": {
"type": "permission_error",
"code": "insufficient_scope",
"message": "This key lacks the approvals:read scope",
"request_id": "8c1f…"
}
}| type | HTTP | When |
|---|---|---|
| invalid_request | 400, 404, 422 | Bad parameters, unknown id; error.fields names each field |
| authentication_error | 401 | Missing, malformed, expired, revoked or wrong-environment key |
| permission_error | 403 | The key lacks the scope the route needs |
| rate_limited | 429 | Over the limit; wait for Retry-After |
| unavailable | 503 | Temporarily unavailable; retry after Retry-After |
| api_error | 500 | Our fault; quote the request_id |
05
Rate limits
300 requests a minute per key, and 600 a minute per client IP. Over either, the API answers 429 with Retry-After; every authenticated response states the key's policy.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
RateLimit-Policy: 300;w=60Please do not poll faster than every five minutes. A CMS should read the feed; anything that must react at once should use webhooks once they are live.
06
Pagination
Lists return { object: "list", data, has_more, next_cursor }, newest change first. limit is 1 to 100 (default 25). Pass next_cursor back as cursor for the next page; it is opaque and stays correct while stories change, so no row is skipped or repeated.
const headers = { authorization: `Bearer ${process.env.ARMAGE_API_KEY}` };
let cursor: string | null = null;
do {
const url = new URL('https://api.arm-age.com/v1/stories');
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('cursor', cursor);
const page = await (await fetch(url, { headers })).json();
for (const story of page.data) console.log(story.id);
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);07
Versioning and deprecation
- The version is in the path:
/v1. - Additive changes need no new version: new routes, new optional parameters, new scopes, new fields in responses, new event types. Ignore fields you do not know.
- Breaking changes get
/v2, and/v1keeps working alongside it. - Nothing is retired without notice: a retiring route answers with
DeprecationandSunsetheaders at least six months before it stops, and the change is in the changelog. - No patient data: Armage does not process protected health information, and no route accepts it.
Questions, a sandbox workspace or a scope you need: hello@arm-age.com.
Build against a sandbox workspace
Or writehello@arm-age.com