The write is idempotent on external_id, so the first import, a scheduled reconcile and a push
after one change are all the same request.
Reconcile at least daily
Send every member who has ever held a certificate, including lapsed ones. Warden treats a record kept current by an integration as stale after 72 hours without a sync, and the public page then says the current status is not verified.
Add a faster pass if a day is too slow
A canceled member reads as active on Warden until your next sync says otherwise. The interval you pick is the longest a lapsed member can look valid.
flowchart TD
A(["A sync says active"]) --> B["The member cancels in your system"]
B -- "the page still says active" --> C("Your next sync")
C -- "sends inactive" --> D(["The page says not active"])
C -- "never arrives, 72 hours" --> E(["The page says the status is not verified"])
class A,D,E warden
class C askSend lapsed members
A member whose printed code still exists should resolve to a page that says the membership is not
active. Send them with normalized_state: inactive.
When a member deletes their account
Erase the record with DELETE /api/v1/integration/entitlements/{external_id}.
It removes the entitlement, the credential with the holder's name, and every identity on it,
including shops the member added through an enrollment invitation. The public address then says
only that the credential is no longer published.
Sending normalized_state: inactive with a placeholder name and identities: [] is not enough for
a deleted account: the credential, its license id and any enrollment identities stay. Erase is
idempotent, so retry it freely; a record already gone answers "found": false.
Pace your requests
A key may make 60 requests a minute, and a 500 record write is one request. A reconcile of a few
thousand members needs a handful. Send batches one after another and honor Retry-After on a 429.
async function reconcile(key: string, members: Member[]) {
for (let i = 0; i < members.length; i += 500) {
const body = JSON.stringify({ records: members.slice(i, i + 500).map(toRecord) })
for (;;) {
const res = await fetch('https://3dwarden.com/api/v1/integration/entitlements', {
method: 'POST', body,
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
})
if (res.status === 429) {
await new Promise((done) => setTimeout(done, Number(res.headers.get('Retry-After') ?? 60) * 1000))
continue
}
if (!res.ok) throw new Error(`${res.status} ${(await res.json()).code}`)
const { rejected } = await res.json()
for (const r of rejected) console.warn('refused', r.external_id, r.because)
break
}
}
}If Warden is unreachable
Skip the run and try again later. Your own verification should keep working without it.