The key is in a cron job, two of your own services, and a partner's system that takes three weeks to change anything. Rotating it means finding an hour when all four switch at once. So it does not get rotated, and the key that was minted for a prototype is still authenticating production traffic two years later.
Rotation should not need a cutover window. Here it does not have one.
Both keys work at the same time
Rotating mints a successor and leaves the old key alone. The old key keeps its own expiry, and rotating does not cut it short. There is no window that closes, no clock you are racing, and nothing to schedule.
So you move senders across one at a time, on whatever day suits each one. The cron job this afternoon, your own services on the next deploy, the partner whenever the partner gets to it.
You can tell who has not moved yet
Each key records when it was last used to authenticate. Before you revoke the old one, look at it. If it last authenticated four days ago and you have been switching senders since, everything has moved. If it authenticated ninety seconds ago, something you forgot is still holding it, and revoking now would break that something.
This is the part that makes the rest safe. Revoking a key is only frightening when you are guessing about who still has it.
Expiry is not a cliff
A key that passes its expiry keeps working for another 24 hours. Requests still succeed in that window, and the response carries a header saying so:
X-Echorelay-Key-Status: expired_grace
X-Echorelay-Key-Successor-Available: true
The second header appears when a successor is already in place and ready to deploy. After the grace window, the key stops: 401, with key_expired as the reason.
If you log one response header from us, log that one. It is the cheapest rotation alarm you will ever build, and it fires a full day before anything breaks.
Revoking is the other tool, and it is blunt on purpose
Revocation is immediate. The next request presenting that key gets a 401 with key_revoked, and there is no grace period, whatever the key's expiry said.
That bluntness is the point. Rotate when a key is merely old. Revoke when it has been pasted into a public repository, and accept that whatever is still using it will break, because that is what you want to happen.
The rotation you do not have to start
If a key has an expiry, you do not have to remember any of this. As the key approaches its expiry, EchoRelay mints the successor itself and holds it for you. The panel, the REST API and the MCP tool will each reveal that successor exactly once. Copy it when you do, because a second look is not possible: the stored copy is destroyed on reveal.
Once the grace window closes on the old key, it is revoked automatically, and the audit log distinguishes that from a revocation you triggered.
One key per sender beats one key per project
Nothing stops you minting a key per sender, each with its own name and its own expiry. It costs nothing and it changes what rotation means: instead of one event that touches every system you own, you get four small independent ones, and the last-used timestamp tells you about a single sender rather than about all of them at once.
If you are going to do one thing after reading this, do that one. It is easier before an incident than during.
What this does not do
The successor's plaintext is shown once and is then unrecoverable. Lose it and your recourse is to rotate again, not to ask us.
This covers the inbound keys your callers present to EchoRelay. The credentials your targets require are a separate thing with a separate lifecycle, and rotating one does not touch the other.
A key with no expiry never enters the automatic path at all, because there is no expiry for a successor to be minted ahead of. Whether editors can create keys like that is a project setting, and the honest default is to keep expiries on.
The reason to rotate on a schedule is not that your key is compromised. It is that you want the procedure to be boring on the day it is.