SMSGatewayCenter Blog

API Key Rotation for an SMS API: Rotating a Single-Slot Key Without Dropping Messages

Most key rotation advice assumes you can run an old key and a new key side by side. On the SMSGatewayCenter API there is one key slot per account, and updating it kills the old key at once. Here is how to rotate it from code anyway: a password-authenticated rotator, a read-back, a versioned secret store, clients that reload on a version change, and a short queue pause so no message is lost.

Featured image for API Key Rotation for an SMS API: Rotating a Single-Slot Key Without Dropping Messages
Abstract illustration of a single key slot where an old token leaves before a new token enters, connected to a row of client nodes
One slot, one key at a time: rotation on this API is a hand-off, not an overlap.

Table of Contents

  1. The Short Answer
  2. TL;DR
  3. Why Dual-Key Rotation Advice Doesn’t Fit Here
  4. The Four API Key Endpoints at a Glance
  5. Reading the Key Back: apikeyList Is Not a List
  6. Which Credential Rotates the Key
  7. The Rotation Sequence, Step by Step
  8. A Rotator in Python
  9. Reload on a Version Change, Not on an Error Code
  10. A Sending Client in Node.js
  11. Pausing the Queue So Nothing Is Lost
  12. Probing a Key Without Sending Anything
  13. Catching Portal Rotations With Fingerprint Drift
  14. When the Key Leaks: Update, Don’t Delete
  15. Delete Is for Decommissioning
  16. What a Key Rotation Doesn’t Touch
  17. How Often to Rotate
  18. Decision Matrix
  19. Rotation Checklist
  20. Unspecified Behaviour and How to Code Around It
  21. FAQs

The Short Answer

You can’t rotate an { the way most guides tell you to, because there’s no moment where the old key and the new key both work. Each account has one key slot. Read API Key returns a single key, and the moment you regenerate it, the old one stops working.

So you rotate it as a hand-off. A small rotator job, authenticated with the account’s userid and password rather than the key it’s about to kill, calls SMSApi/apikey/update, reads the new key back with SMSApi/apikey/read, checks it with a read-only call, and writes it to your secret store under a new version number. Your senders reload the key whenever the version changes. If you pause the send queue for those few seconds, not a single message fails.

TL;DR

  • One slot, no overlap. The read endpoint returns count: 1 and an object called apikeyList holding one apikey. Regenerating the key invalidates the previous one straight away.
  • Update and create don’t hand you the key. Both return a plain success message. You always fetch the new value with SMSApi/apikey/read.
  • Rotate with the password, not the key. A rotator that authenticates with the key it’s rotating locks itself out halfway through. Keep userid and password in a separate secret path that only the rotator can read.
  • Version the secret. Store the key with a version number and a short SHA-256 fingerprint. Clients compare versions, never parse error messages.
  • Reload on a version change. When a send fails, the client re-reads the secret. New version: retry once. Same version: it isn’t a rotation problem, so handle it like any other failure.
  • Pause, rotate, resume. A queue-based sender holds messages for a few seconds instead of failing them. That’s how you get zero lost messages without an overlap window.
  • Probe without sending. Check a new key with SMSApi/account/readstatus, which costs nothing and sends nothing.
  • Watch for drift. Anyone who regenerates the key in the dashboard breaks production. Compare the live key’s fingerprint with your store’s on a schedule.
  • On a leak, update, then change the password. Update kills the leaked key and gives you a new one in one step. Delete is for when you’re retiring the key for good.

Why Dual-Key Rotation Advice Doesn’t Fit Here

Search for “API key rotation without downtime” and almost every answer describes the same pattern. Issue a second key, deploy it everywhere, wait until nobody’s using the first one, then revoke it. The safety comes from the overlap window, a stretch of time where both keys are valid.

That pattern needs a provider that lets you hold two live keys. This one doesn’t. The read endpoint returns one key in an object, not an array. The regeneration help page is blunt about it: regenerating “will invalidate the previous key, so any existing integrations using the old key will stop working.” There’s no grace period to configure and no second slot to stage into.

That changes the problem. You aren’t trying to overlap two keys. You’re trying to make the gap between “old key dead” and “every sender has the new key” short, safe and invisible to customers. Three things get you there:

  1. A single rotator that owns the slot, so two processes never regenerate at once.
  2. A versioned secret that every sender reloads when it changes, so the gap is as long as one secret read.
  3. A queue that can pause, so whatever does fall in the gap waits instead of failing.

If you build only the first two, you’ll see a burst of failed sends at every rotation, sized by your traffic and your reload speed. Add the third and the burst goes to zero.

Three-panel flow showing a rotator authenticating with userid and password, the platform's single API key slot, and a versioned secret store feeding sending clients
The rotator never uses the key it’s rotating. It updates the slot, reads the new key back, probes it, and only then publishes a new version.

The Four API Key Endpoints at a Glance

All four live under https://unify.smsgateway.center/SMSApi/apikey/. They take only your login credentials and output. None of them takes a key value as a parameter.

EndpointMethods on its pageWhat it returnsWhat it does to the slot
apikey/createPOST and GETstatus, msg “ApiKey Created successfully.”, code “200”Creates a key. The value isn’t in the response.
apikey/readPOST and GETcount 1 and apikeyList holding apikeyNothing. Read-only.
apikey/updatePOST onlystatus, msg “ApiKey updated successfully.”, code “200”Replaces the key. The old key stops working.
apikey/deletePOST and DELETEstatus, msg “ApiKey Deleted successfully.”, code “200”Removes the key. Only userid and password still work.

A few things jump out once they’re side by side.

None of the writes returns the key. If your code expects update to hand back the new value, it’ll get a success message and nothing to store. The read is always the second half of every write.

The methods differ. update is POST only. delete accepts POST and DELETE. create and read accept POST and GET. Use POST for all four and you never have to remember which is which.

Every response nests under response with api “apikey” and an action that names the operation. Check response.status, and treat the action name as a sanity check that you hit the endpoint you meant to.

Four-column comparison of the create, read, update and delete API key endpoints showing methods, response contents and the effect on the single key slot
Only the read returns a key. Every write needs a read after it.

Reading the Key Back: apikeyList Is Not a List

Here’s the read response, verbatim from the endpoint’s page:

{
  "response": {
    "api": "apikey",
    "action": "read",
    "status": "success",
    "msg": "success",
    "code": "200",
    "count": 1,
    "apikeyList": {
      "apikey": "902be2ba73523ek3k32c5f0b5f8f684337f710bc4"
    }
  }
}

Two traps sit in those eight lines.

The name apikeyList says array. The value is an object. Code written against the name (response.apikeyList[0].apikey) gets undefined in JavaScript, a KeyError in Python and a class cast exception in Java. Code written against the sample works.

The sample key also isn’t hex. It’s 41 characters and contains a k, so a validator that insists on [0-9a-f] or on 32 or 40 characters will reject a perfectly good key. Don’t validate the alphabet or the length. Check that it’s a non-empty string with no whitespace, then prove it works with a probe.

Parse it defensively, so a future change from object to one-element array doesn’t take your rotator down. This Java version accepts both shapes and refuses anything else:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public final class ApiKeyReadParser {
    private static final ObjectMapper MAPPER = new ObjectMapper();

    /** Returns the key, or throws if the response isn't a usable success. */
    public static String extractKey(String body) throws Exception {
        JsonNode r = MAPPER.readTree(body).path("response");
        if (!"success".equals(r.path("status").asText())) {
            throw new IllegalStateException("apikey/read did not succeed");
        }
        JsonNode list = r.path("apikeyList");
        JsonNode entry = list.isArray() ? list.path(0) : list; // object today, array tomorrow
        String key = entry.path("apikey").asText("");
        if (key.isEmpty() || !key.equals(key.strip()) || key.chars().anyMatch(Character::isWhitespace)) {
            throw new IllegalStateException("apikey/read returned no usable key");
        }
        return key;
    }
}

Note what it doesn’t do. It doesn’t look at code, count or msg. count is 1 here even though there’s nothing to count, and msg is free text. Status at the right nesting level, then the payload, then stop.


Which Credential Rotates the Key

Every API key endpoint accepts either form credentials (userid plus password) or the apikey header. That choice matters more for rotation than for anything else.

Say the rotator authenticates with the key. It calls update, which succeeds and kills that key. Now it calls read to fetch the new value, using the same key in the header. That call fails, because the key is dead. You’ve rotated into a state where nothing you control knows the current key, and you have to go to the dashboard by hand.

So the rotator authenticates with userid and password for every apikey/* call. Those credentials don’t change when the key does. Your senders never see the password. They only ever get the key.

That gives you two secrets with very different jobs:

SecretWho reads itUsed forRotated by
userid + passwordThe rotator onlyapikey/update, apikey/read, drift checksA person, rarely, through the account settings
apikeyEvery senderSMSApi/send and all other trafficThe rotator, on a schedule or on demand

Keep them in separate paths in your secret store with separate access policies. If a sender host is compromised, the attacker gets the key, which you can rotate in seconds. They don’t get the password, which is your way back in.

The same split applies if you use the account’s key for anything else your team runs, like a reporting job or a CRM connector. They’re senders. They read the key from the store like everything else and never hold the password.


The Rotation Sequence, Step by Step

Here’s the whole thing in order. Every step is there for a reason, and the reasons are mostly about what happens when a step fails halfway.

  1. Take the rotation lock. One rotator at a time. Two rotators racing each other will each regenerate the key, and whichever reads back first publishes a key that’s already dead.
  2. Pause the send queue and let in-flight calls finish. Stop pulling new jobs, wait for the requests already on the wire to return. A few seconds is usually plenty.
  3. Call SMSApi/apikey/update with userid and password. Check response.status. From here on, the old key is gone.
  4. Call SMSApi/apikey/read with the same credentials. Pull out response.apikeyList.apikey. This is the only place the new value ever appears.
  5. Probe the new key without sending anything. Call SMSApi/account/readstatus with the new key in the apikey header and check for success.
  6. Publish it as a new version. Write the key, version N+1, its fingerprint and a timestamp to your secret store in one write.
  7. Resume the queue. Senders pick up the new version on their next job or their next failure, whichever comes first.
  8. Log the rotation and release the lock. Old fingerprint, new fingerprint, who or what triggered it, and why. Never the key itself.

What if the job dies between steps 3 and 6? You don’t know whether update landed, and senders are holding a key that may be dead. Don’t guess. Restart from step 4: read the live key, compare its fingerprint with the one in your store, and if they differ, carry on from step 5 with the live key. The read-back answers the question for the cost of one call, whatever state the slot ended up in.

The same rule covers a timeout on step 3. A timed-out update may or may not have happened. Reading the slot tells you which, so a rotator that always reads after a write never has to reason about partial failure.


A Rotator in Python

This is the rotator from the sequence above, with the secret store and queue left as small interfaces you’d back with whatever you already run. The trap it guards against is the one from the previous section: every apikey/* call goes out with userid and password in the form body, never with the key header.

import hashlib
import time
import requests

BASE = "https://unify.smsgateway.center/SMSApi"

def fingerprint(key: str) -> str:
    """Short, non-reversible label for logs and drift checks. Never log the key."""
    return hashlib.sha256(key.encode("utf-8")).hexdigest()[:12]

def _ok(body: dict) -> bool:
    return isinstance(body, dict) and body.get("response", {}).get("status") == "success"

class Rotator:
    def __init__(self, userid, password, store, queue, timeout=15):
        self.creds = {"userid": userid, "password": password, "output": "json"}
        self.store = store      # get() -> {"version", "key", "fp"}; put(dict)
        self.queue = queue      # pause(), wait_idle(seconds), resume()
        self.timeout = timeout

    def _post(self, path, data=None, headers=None):
        resp = requests.post(f"{BASE}/{path}", data=data, headers=headers, timeout=self.timeout)
        try:
            return resp.json()
        except ValueError:
            return {}

    def read_live_key(self) -> str:
        body = self._post("apikey/read", data=self.creds)
        if not _ok(body):
            raise RuntimeError("apikey/read failed")
        entry = body["response"].get("apikeyList")
        if isinstance(entry, list):          # tolerate a future array shape
            entry = entry[0] if entry else {}
        key = (entry or {}).get("apikey", "")
        if not isinstance(key, str) or not key or any(c.isspace() for c in key):
            raise RuntimeError("apikey/read returned no usable key")
        return key

    def probe(self, key: str) -> bool:
        body = self._post("account/readstatus",
                          data={"userid": self.creds["userid"], "output": "json"},
                          headers={"apikey": key})
        return _ok(body)

    def publish(self, key: str, reason: str) -> dict:
        current = self.store.get() or {"version": 0, "fp": None}
        record = {"version": current["version"] + 1, "key": key,
                  "fp": fingerprint(key), "rotated_at": int(time.time()),
                  "reason": reason, "previous_fp": current["fp"]}
        self.store.put(record)
        return record

    def rotate(self, reason="scheduled") -> dict:
        self.queue.pause()
        try:
            self.queue.wait_idle(seconds=30)
            try:
                updated = _ok(self._post("apikey/update", data=self.creds))
            except requests.RequestException:
                updated = False                  # unknown outcome; the read decides
            live = self.read_live_key()
            stored = self.store.get() or {}
            if not updated and fingerprint(live) == stored.get("fp"):
                raise RuntimeError("update did not change the key; nothing published")
            for _ in range(3):                   # allow a moment for the new key to settle
                if self.probe(live):
                    break
                time.sleep(2)
            else:
                raise RuntimeError("new key failed the probe; not publishing")
            return self.publish(live, reason)
        finally:
            self.queue.resume()

A few choices in there are deliberate.

rotate() resumes the queue in a finally. If anything throws, senders come back up with whatever key the store holds. If that key is dead, they’ll fail, reload, find the same version and treat it as a real failure, which is what you want: a loud alert, not a silent stall.

The probe gets three tries, two seconds apart, before the rotator gives up. A brand-new key that fails once and passes a moment later is a settling delay, not a bad key.

The update result is advisory. What gets published is decided by the read-back and the probe, never by the update’s msg.

The probe passes userid in the body alongside the apikey header. The key replaces the password, not the user ID.

Run it from a scheduler with a distributed lock around rotate(). The lock is your step 1.


Reload on a Version Change, Not on an Error Code

The obvious way to make senders rotation-aware is to spot the “invalid key” error and reload. Don’t. You’d be branching on an error code or parsing a reason string, and both are free to change. You’d also misfire: plenty of failures that look like auth problems aren’t rotations.

There’s a better signal, and you own it: the version number in your secret store.

When a send fails, the client re-reads the secret. Then it asks one question. Is the version newer than the one I just used?

  • Yes. A rotation happened while I was holding the old key. Retry this send once with the new key.
  • No. Whatever went wrong, it wasn’t a rotation. Hand the failure to your normal retry and error handling, exactly as you would have without any of this.

That rule works whatever the platform’s auth failure looks like, because it never looks at it. It also can’t loop: a retry happens at most once, and only when the version moved.

There’s one more condition on the retry. Only retry when the failed response carries no transactionId. A response with an ID means the platform accepted something, and retrying it could send the message twice. Requests that fail authentication shouldn’t produce an ID, but you don’t have to rely on that: if there’s an ID, don’t retry, full stop. Your idempotency layer covers anything this rule misses. The duplicatecheck parameter on SMSApi/send won’t, because it removes duplicate numbers inside one request, not repeats across two.

Between failures, senders can also refresh the secret on a timer, say every 30 seconds, so most of them have the new key before they ever use the old one. With the queue paused during rotation, the timer alone is usually enough. The reload-on-failure path is the backstop.


A Sending Client in Node.js

Here’s the client half. The trap this one guards against is a retry loop: without the version check, a send that fails for any reason would reload and retry forever.

const BASE = "https://unify.smsgateway.center/SMSApi";

class KeyedSender {
  constructor(userid, secretStore) {
    this.userid = userid;
    this.store = secretStore;      // async get() -> { version, key }
    this.current = null;
  }

  async loadKey() {
    this.current = await this.store.get();
    return this.current;
  }

  async postSend(fields, key) {
    const body = new URLSearchParams({ userid: this.userid, output: "json", ...fields });
    const res = await fetch(`${BASE}/send`, {
      method: "POST",
      headers: { apikey: key, "content-type": "application/x-www-form-urlencoded" },
      body,
    });
    try { return await res.json(); } catch { return {}; }
  }

  async send(fields) {
    const used = this.current ?? (await this.loadKey());
    const first = await this.postSend(fields, used.key);
    if (first.status === "success") return first;

    // Never retry something the platform may have accepted.
    if (first.transactionId) return first;

    const fresh = await this.loadKey();
    if (fresh.version <= used.version) return first; // not a rotation; normal failure path

    return this.postSend(fields, fresh.key);          // exactly one retry, new key
  }
}

module.exports = { KeyedSender };

Call it with the same fields you’d send anyway: sendMethod, mobile, msg, senderid, msgType, and your DLT fields. Nothing about rotation leaks into the calling code.

Two things to keep straight. status is read at the top level because a quick send returns a flat object; if you send by group or file, read status wherever that response shape puts it, following the same rule as the rest of your client. And fields never contains password. The sender doesn’t have it.


Pausing the Queue So Nothing Is Lost

Even with fast reloads, there’s a window between update succeeding and the last sender picking up the new key. Every send in that window fails once. The reload-and-retry path rescues most of them, but “most” isn’t the goal.

If your sends already go through a queue (and if you’re sending at any volume they should, for rate limiting and backpressure reasons alone), the fix is cheap: stop consuming during the rotation.

  1. The rotator flips a pause flag that workers check before pulling a job.
  2. Workers finish the request they’re on and go idle. The rotator waits until in-flight count is zero, or a timeout passes.
  3. Rotation runs: update, read, probe, publish.
  4. The rotator clears the flag. Workers reload the secret before their first job, because the version moved.

Messages queued during those seconds just wait. Nothing fails, nothing retries, nothing goes out twice. For OTPs that latency matters, so do it during your quietest hour and keep the pause short. A rotation that reads, probes and publishes in a couple of seconds is a couple of seconds of OTP delay, once a month.

If you can’t pause (a synchronous API where the caller is waiting on the send), skip step 1 and rely on reload-and-retry. You’ll see a handful of single retries per rotation. That’s fine as long as you can see them, which is what the next sections are about.


Probing a Key Without Sending Anything

You need a way to ask “does this key work?” that doesn’t send a message, doesn’t cost anything and doesn’t change state. SMSApi/account/readstatus fits: it returns account status including smsBalance, and it accepts the apikey header like everything else.

A raw probe from the shell looks like this. The trap here is the header name: send the key as an apikey header, not as Authorization: Bearer, and keep userid in the body.

curl -s -X POST "https://unify.smsgateway.center/SMSApi/account/readstatus" \
  -H "apikey: ${SMSGC_APIKEY}" \
  --data-urlencode "userid=${SMSGC_USERID}" \
  --data-urlencode "output=json"

HTTP header names are case-insensitive under RFC 9110, so apikey and apiKey reach the server the same way. The value is what matters.

Use the probe in three places: in the rotator before publishing, in a health check every few minutes so a dead key pages someone before customers notice, and after any manual change in the dashboard. Don’t use apikey/read as a health check. It works, but it puts the key on the wire in a response body, and a health check runs often enough to make that a habit you don’t want.


Catching Portal Rotations With Fingerprint Drift

Your rotator isn’t the only thing that can change the key. Anyone with dashboard access can regenerate it from the API key settings page, and the help page for that flow tells them to “blank out the API key value” and save. One well-meaning click there and every sender you run is holding a dead key, with nothing in your secret store to say why.

Catch it with a drift check. Every fifteen minutes or so, the rotator (it has the password, so it’s the right job for this) reads the live key and compares fingerprints:

def drift_check(rotator, alert):
    live = rotator.read_live_key()
    stored = rotator.store.get() or {}
    if fingerprint(live) == stored.get("fp"):
        return "in_sync"
    if not rotator.probe(live):
        alert("live key fails probe; manual attention needed")
        return "broken"
    record = rotator.publish(live, reason="adopted_drift")
    alert(f"key changed outside the rotator; adopted as v{record['version']}")
    return "adopted"

Adopting the live key gets senders working again straight away. The alert is the important part, though. A key that changed without your rotator is either a teammate who didn’t know, or someone who had the old key and used it to call apikey/update. You can’t tell which from the API, so treat an unexplained drift as a possible leak and follow the next section.

While you’re in the dashboard settings: the create form lets you type your own key value instead of leaving it blank for a random one. Don’t. A human-chosen key is guessable in ways a generated one isn’t, and nothing stops two environments ending up with the same value. Always let the platform generate it.


When the Key Leaks: Update, Don’t Delete

A key in a public repo, a log line, a screenshot in a ticket. Whatever the route, the first move is the same: run the rotator with reason="leak". Don’t wait for a quiet hour.

Why update and not delete? Both kill the leaked key. But delete leaves the slot empty, so your senders are down until someone runs create and read. update kills the leaked key and puts a fresh one in the slot in one call, and the rotator has it published seconds later. Same security result, no extra outage.

Then work through the rest:

  1. Change the account password. A key authorises everything the account can do through the API. Assume the holder tried more than sending, and close the second door too. Update the rotator’s secret path right after.
  2. Check what the key could have changed. Read back your webhook with SMSApi/webhook/read and compare smsWebhook with the URL you expect. A redirected webhook quietly ships your delivery reports to someone else. Do the same for sender IDs and templates with their read endpoints.
  3. Look for traffic you didn’t send. Pull delivery reports for the exposure window with SMSApi/reports/status and diff them against your own outbound message table. Anything on the platform’s side that isn’t on yours went out under the leaked key.
  4. Find the leak and close it. A key in a log means a logger that prints headers. A key in a repo means a config file that shouldn’t be committed. Fix the cause or you’ll be doing this again.
  5. Record it. Old fingerprint, new fingerprint, when the leak started, when the key died, what you found in steps 2 and 3.

If the rotator itself can’t run (the password was in the same leak, say), rotate from the dashboard, then let the drift check adopt the new key once you’ve updated the rotator’s credentials.


Delete Is for Decommissioning

SMSApi/apikey/delete has a narrower job than it looks. Use it when you’re done with key authentication for that account: you’ve moved a product onto OAuth, or you’re shutting down an integration and want nothing left that can authenticate as it.

Before deleting, make sure nothing still reads the key from your store. After deleting:

  1. Probe with the old key. It should fail. If it succeeds, the delete didn’t take, so stop and investigate.
  2. Mark the secret in your store as retired rather than removing it, so a sender that reloads gets a clear “no key” instead of a stale one.
  3. Leave the drift check running for a week. A key that reappears means somebody created one by hand.

If you need key auth again later, create and then read gets you back to the start of this whole cycle.


What a Key Rotation Doesn’t Touch

Rotating the API key changes one credential. Several other secrets sit near it, and it’s easy to assume they move together.

ThingChanges when the API key rotates?What to do
Account userid and passwordNoRotate separately, by hand, after a leak or a staff change
Message encryption tokenNo. It’s a separate token from the API key.Rotate on its own schedule; see SMS message encryption
Your inbound callback URL secretNo. It’s yours, not the platform’s.Rotate when you change the callback URL
Webhook URL and rateNoRead back after a leak in case it was altered
OAuth access and refresh tokensUnclearProbe with a token after rotating; see the next sections
Scheduled SMS bookingsUnclearCompare SMSApi/schedule/read before and after

The encryption row trips people up most. The encryption token help page is explicit that the two are separate. Rotating the API key does nothing for message confidentiality, and rotating the encryption token does nothing for API access.


How Often to Rotate

The key doesn’t expire on its own, so the schedule is yours. Two kinds of trigger cover it.

Scheduled. Monthly is a sensible default once the rotator is automated. It keeps the code path exercised, so the day you need it in a hurry, you already know it works. If rotation still involves a person and a dashboard, you’ll rotate far less often than you should; the OWASP Secrets Management Cheat Sheet makes the same point about manual rotation being where the mistakes come from.

Event-driven. Rotate right away when someone who could read the key leaves the team, when a vendor or contractor with access is offboarded, when a host that held the key is rebuilt after an incident, and on any suspected leak.

Rotating weekly or daily buys little extra if the key never leaves your secret store, and each rotation is a short pause in sending. If you want shorter-lived credentials than that, the answer isn’t faster key rotation; it’s a different mechanism, such as OAuth with its one-hour access tokens.


Decision Matrix

SituationDo thisDon’t do this
Routine monthly rotationRotator with queue pause, quietest hourRegenerate in the dashboard and paste the key into config
Key seen in a repo, log or ticketRotator with reason="leak" now, then change the passwordWait for the scheduled rotation
Someone regenerated the key in the dashboardLet the drift check adopt it, then ask who and whyTreat it as harmless because things work again
Moving an integration to OAuthMigrate senders, then apikey/delete, then probeDelete first and migrate after
Synchronous sends, no queueRotate with reload-and-retry only, expect a few single retriesRetry on any failure without the version check
Sends failing and the version hasn’t movedNormal failure handling, check the probeAssume a rotation and keep reloading
Building a SaaS that sends for customersUse OAuth instead of holding their keysStore each customer’s API key and rotate it for them
Need credentials that expire on their ownOAuth access tokensRotating the API key daily

Rotation Checklist

Before the first rotation

  • userid and password stored in a secret path only the rotator can read
  • API key stored with version, fp, rotated_at and reason
  • Every sender reads the key from the store, none from a config file or environment baked into an image
  • Senders refresh on a timer and reload on failure, retrying only when the version moved and there’s no transactionId
  • A distributed lock around the rotator
  • Queue workers honour a pause flag and report in-flight count
  • Probe on SMSApi/account/readstatus wired into a health check
  • Logs scrubbed of the apikey header; fingerprints only

During a rotation

  • Lock taken, queue paused, in-flight drained
  • apikey/update called with userid and password
  • New key read back from apikey/read, never taken from the update response
  • Probe passes with the new key
  • New version published, queue resumed, lock released

After a rotation

  • Old key fails the probe
  • Scheduled bookings in SMSApi/schedule/read match what was there before
  • Any OAuth tokens you hold still work
  • Rotation logged with both fingerprints

Unspecified Behaviour and How to Code Around It

A few behaviours around the key slot aren’t pinned down by anything you can read ahead of time. Each item below gives you the choice that stays safe whichever way the platform actually behaves, and none of them needs you to wait for an answer before shipping.

One. Read the slot after every write, including create. What create does on an account that already has a key, overwrite it or refuse, isn’t something you can know in advance. If you always read back and compare fingerprints, you don’t need to: the read tells you what’s in the slot now, and that’s all your store needs.

Two. Treat a timed-out update as “maybe”. A timeout tells you nothing about whether the key changed. The rotator above reads the slot whatever happened, so a lost response costs one extra call, never a split brain between your store and the platform.

Three. Accept apikeyList as an object or a one-element array. It’s an object today with a name that suggests a list. Parsing both shapes and rejecting anything else costs two lines and survives a fix in either direction.

Four. Validate the key by probing it, not by its shape. Nothing fixes the key’s length or alphabet, and the sample isn’t hex. Non-empty, no whitespace, passes the probe: that’s the whole test.

Five. Decide retries by the version number, not by the auth error. The exact payload for a rejected key isn’t something you should build on. A client that only retries when your own secret store’s version moved never needs to recognise it.

Six. Never retry a response that carries a transactionId. Whether an auth failure could ever come back with an ID is unknowable from outside. Refusing to retry anything with an ID makes the question irrelevant, and your idempotency layer catches the rest.

Seven. Probe your OAuth tokens after each rotation. Whether OAuth access and refresh tokens are tied to the API key isn’t something you can rely on either way. Probe with a token after rotating. If it fails, your OAuth refresh path marks the connection for re-authorisation, which it has to handle anyway.

Eight. Compare scheduled bookings before and after rotating. Whether a booking made through SMSApi/send with scheduleTime survives a key rotation isn’t something to assume. Snapshot the uuId list from SMSApi/schedule/read before you rotate and diff it after. If anything’s missing, rebook it from your own scheduler.

Nine. Change the password after any leak, even if you think the key couldn’t touch it. What a key holder can and can’t change about the account is a question you don’t have to answer. Changing the password closes the door whichever answer is true.

Ten. Run exactly one rotator per account. Whether two update calls a second apart are handled cleanly on the platform side doesn’t matter if they never happen. A lock on your side guarantees it.

Eleven. Give a brand-new key a few seconds before declaring it bad. Whether a regenerated key is valid everywhere at the instant update returns isn’t something to bet on. Three probes, two seconds apart, separates a settling delay from a real failure.

Twelve. Treat the old key as dead the moment you call update, even if it still works. If an old key keeps working briefly, nothing in this design notices or cares. If it dies instantly, nothing breaks either, because senders reload on the version change.


FAQs

Can I have two API keys active at the same time on SMSGatewayCenter?

No. The read endpoint returns one key, and regenerating it invalidates the previous one straight away. Plan rotation as a hand-off between one key and the next, using a versioned secret and a short queue pause, rather than an overlap.

Does SMSApi/apikey/update return the new key?

No. It returns status, msg “ApiKey updated successfully.” and code “200”. Call SMSApi/apikey/read straight after and take the key from response.apikeyList.apikey.

Which credential should call the API key endpoints?

Your userid and password. If you authenticate update with the key itself, the key dies mid-rotation and you can’t read the new one with it. Keep the password in a secret only the rotator can see.

How do I send the API key on normal requests?

As an apikey HTTP header, with userid still in the request body and no password. Header names are case-insensitive, so apikey and apiKey are the same thing to the server.

Will rotating the key cause downtime?

Not if your sends go through a queue. Pause consumption, rotate, publish the new version and resume. Messages wait a few seconds instead of failing. Without a queue, expect a few sends to fail once and succeed on the reload-and-retry.

Does rotating the API key also rotate my message encryption token?

No. They’re separate secrets. Rotate the encryption token on its own schedule.

Should I use apikey/delete when a key leaks?

Use apikey/update instead. Both kill the leaked key, but update puts a fresh one in the slot in the same call, so your senders are back within seconds. Save delete for retiring key authentication altogether.

How often should I rotate the key?

Monthly on a schedule once it’s automated, plus right away when someone with access leaves, when a vendor is offboarded, or when you suspect a leak. If you need credentials that expire on their own, use OAuth rather than rotating the key every day.

Someone regenerated the key in the dashboard and everything broke. What now?

Run the drift check: read the live key with the password, probe it, and publish it as a new version so senders reload. Then find out who changed it and why, and treat it as a possible leak until you know.


Want a Second Look at How You Store API Keys?

Running SMS, WhatsApp or RCS traffic from your own backend and want a second pair of eyes on how you hold and rotate credentials? Start with the developer API reference, or get in touch with our team to talk through your integration.


More from our Blog

SMS Message Encryption: What It Hides, What It Doesn’t, and How to Get the Wire Format Right

SMS Click Tracking and Attribution: Joining Smart Link Clicks to Messages and Recipients

Consent Capture and Evidence for SMS, WhatsApp, RCS and Telegram: Building an Opt-In Record You Can Prove

Two-Way SMS Keyword Design: Routing, Auto-Replies and Conversation State on a Long Code

Opt-Out and Suppression Across SMS, WhatsApp, RCS and Telegram: Building the List That Actually Stops Sends


Save this interesting page on your favorite Social Media

Blog Author logo

SMS Gateway Center Desk

SMS Gateway Center is one of the largest and leading SMS Provider in India. It is run by a large professional team to cater small companies to large corporate companies. SMS Gateway Center is associated with the best operators in India covering the entire states in India. SMS Gateway Center has been serving through its SMS Resellers in more than 20 states in India. To become our SMS Reseller, kindly contact us

Looking for the best business communication solutions, get in touch!