Getting started
Authentication
Halyard authenticates every request with an API key sent as a Bearer token. Use test keys while you build, restricted keys in production, and rotate without downtime.
On this page
API keys
Each project has its own keys, created in the dashboard under Developers → API keys or with the Keys API. A key identifies the project, the environment and what the caller may do. Every key has a public ID such as key_2Mx8Rc6Jd3 that is safe to log, and a secret you only see once.
hy_test_51Hq8Zx2rTmV9cKpL3d
- hy_Halyard secret key
- test_Environment: test (sandbox)
- 51Hq…Random part, unique per secret
Test and live mode
The prefix tells you the environment. Test and live data are completely separate: a job created with a test key can only be seen and worked by test keys.
| Prefix | Environment | Behavior |
|---|---|---|
| hy_test_ | Test | Sandbox. Free, rate-limited to 25 req/s, data deleted after 30 days. |
| hy_live_ | Live | Production. Billed, full rate limits, data kept per your retention plan. |
Sending the key
Send the secret in the Authorization header using the Bearer scheme. All requests must use HTTPS; plain HTTP is refused before authentication. The SDKs set the header for you.
GET /v3/jobs?limit=3 HTTP/1.1Host: api.halyard.exampleAuthorization: Bearer hy_test_51Hq8Zx2rTmV9cKpL3dHalyard-Version: 2026-09-01curl https://api.halyard.example/v3/jobs?limit=3 \ -H "Authorization: Bearer hy_test_51Hq8Zx2rTmV9cKpL3d"import { Halyard } from "@halyard/node"; // Reads HALYARD_KEY when no key is passedconst halyard = new Halyard(process.env.HALYARD_KEY);import osfrom halyard import Halyard # Reads HALYARD_KEY when no key is passedhalyard = Halyard(os.environ["HALYARD_KEY"])// Reads HALYARD_KEY when the key is emptyclient := halyard.New(os.Getenv("HALYARD_KEY"))# Reads HALYARD_KEY when no key is passedhalyard = Halyard::Client.new(ENV.fetch("HALYARD_KEY"))Restricted keys and scopes
A secret key without restrictions can do everything in its environment. For anything that runs in production, create a restricted key with only the scopes it needs, and optionally limit it to specific queues. A worker that only sends email needs jobs:work on emails and nothing else.
| Scope | Grants | Typical caller |
|---|---|---|
| jobs:read | List and retrieve jobs, attempts and logs. | Dashboards, status pages |
| jobs:write | Create, cancel and retry jobs. Includes jobs:read. | Web servers enqueuing work |
| jobs:work | Claim jobs, heartbeat and report results. | Worker processes |
| events:publish | Publish events to topics. | Services emitting domain events |
| events:subscribe | Create and change event subscriptions. | Deploy pipelines |
| schedules:write | Create, pause and delete schedules. | halyard deploy in CI |
| queues:manage | Change queue limits, pause and drain queues. | Operators, CI |
| keys:manage | Create, roll and revoke API keys. | Admin tooling only |
curl https://api.halyard.example/v3/keys \ -H "Authorization: Bearer hy_test_51Hq8Zx2rTmV9cKpL3d" \ -H "Content-Type: application/json" \ -d '{ "name": "email-worker (production)", "scopes": ["jobs:work"], "queues": ["emails"], "expires_at": "2027-10-01T00:00:00Z" }'const key = await halyard.keys.create({ name: "email-worker (production)", scopes: ["jobs:work"], queues: ["emails"], expires_at: "2027-10-01T00:00:00Z",}); console.log(key.id); // key_2Mx8Rc6Jd3console.log(key.secret); // shown once, store it nowkey = halyard.keys.create( name="email-worker (production)", scopes=["jobs:work"], queues=["emails"], expires_at="2027-10-01T00:00:00Z",) print(key.id) # key_2Mx8Rc6Jd3print(key.secret) # shown once, store it nowRotating keys
Roll a key on a regular schedule, when someone with access leaves, or immediately if you think it leaked. Rolling issues a new secret for the same key ID, so scopes and audit history stay intact.
Roll the key with an overlap window
Set
expires_inso the old secret keeps working while you deploy. Use0to revoke it at once if the key leaked.curl https://api.halyard.example/v3/keys/key_2Mx8Rc6Jd3/roll \ -H "Authorization: Bearer hy_test_51Hq8Zx2rTmV9cKpL3d" \ -d expires_in=86400const rolled = await halyard.keys.roll("key_2Mx8Rc6Jd3", { expires_in: 86400, // old secret keeps working for 24 hours}); console.log(rolled.secret); // the new secretrolled = halyard.keys.roll( "key_2Mx8Rc6Jd3", expires_in=86400, # old secret keeps working for 24 hours) print(rolled.secret) # the new secretDeploy the new secret
Update the secret in your host or CI and redeploy every service and worker that uses it.
Confirm the old secret is unused
The key's page in the dashboard shows Last used per secret. When the old one shows no traffic, let it expire or revoke it early.
Keeping keys secret
- Store keys in your host's secret manager or environment, never in code.
- Use one restricted key per service, so revoking one never takes down the others.
- Set
expires_aton keys for contractors, demos and one-off scripts. - Log the key ID, not the secret. The SDKs redact secrets in their own debug logs.
Authentication errors
A missing, malformed or revoked key returns 401. A valid key without the needed scope or queue access returns 403. Both include a request_id to quote when you contact support.
{ "error": { "type": "authentication_error", "code": "invalid_api_key", "message": "The API key provided is not valid. Check that it is complete and has not been revoked.", "request_id": "req_6Gt3Yb8Qe2", "doc_url": "https://docs.halyard.example/docs/authentication#auth-errors" }}{ "error": { "type": "permission_error", "code": "missing_scope", "message": "This key does not have the 'schedules:write' scope required for POST /v3/schedules.", "required_scope": "schedules:write", "key_id": "key_2Mx8Rc6Jd3", "request_id": "req_6Gt3Yb8Qe2" }}| Status | code | What to check |
|---|---|---|
| 401 | missing_api_key | No Authorization header, or the scheme is not Bearer. |
| 401 | invalid_api_key | Typo, truncated secret, or a revoked or expired key. |
| 401 | environment_mismatch | A test key used on a live-only resource, or the reverse. |
| 403 | missing_scope | Add the scope named in required_scope, or use another key. |
| 403 | queue_not_allowed | The key is limited to other queues. |