warden

Integrate

Send records

Every field of an entitlement record, what Warden does with it, and what it refuses.

One request does everything: the first import, a scheduled reconcile and a push after one change. It is idempotent on external_id.

POST /api/v1/integration/entitlements
Authorization: Bearer wik_...
Content-Type: application/json

{ "records": [ { ... }, { ... } ] }

At most 500 records in a request. Send requests one after another. The body is records and nothing else.

A full record

{
  "external_id": "member-1001",
  "status": "active",
  "normalized_state": "active",
  "tier": "commercial",
  "capabilities": ["physical_sales"],
  "effective_from": "2025-12-03T01:02:03Z",
  "expires_at": null,
  "credential": {
    "title": "Commercial license",
    "holder_name": "Example Print Shop",
    "registration_id": "EX-1A67D8F2",
    "registered_at": "2025-12-03T01:02:03Z",
    "publish_terms": false,
    "retired": false
  },
  "identities": [
    { "source": "website", "url": "https://shop.example.com", "public": true },
    { "source": "yourshop_storefront", "handle": "example", "url": "https://yourshop.example/s/example", "public": true, "assurance": "issuer_hosted" }
  ]
}

external_id, status and normalized_state are required. Everything else is optional.

State

status is your own wording, up to 200 characters. Warden stores it and never interprets it. Use a short, coarse word such as active, lapsed, revoked or withdrawn. Do not put a customer identifier, a payment provider's state or a staff note in it. Warden never shows status outside your organization, and it is not quoted in a message to a seller.

normalized_state is active, inactive or unknown, and you decide it. Warden will not work it out from status, because a word like "approved" means different things in different systems. Define active as whatever your own verification page would call valid at the moment you send the record.

tier is your own label, up to 200 characters, stored and not interpreted.

effective_from and expires_at are ISO 8601 dates. Warden stores them as instants in UTC.

Capabilities

capabilities is what the membership grants, from a closed list. Today the list has one value:

ValueMeaning
physical_salesThe member may sell physical prints of the creator's designs.

Send what the membership grants whether or not it is currently in force. State and capability are separate facts, and Warden never derives one from the other. A value outside the list is refused. If your program grants something the list does not cover, ask for it to be added, and until then leave it out.

Credential

credential creates the public verification page. A record sent without one has an entitlement and no public page. Leaving credential out of a later sync changes nothing, and the page stays. To withdraw it, send retired: true, or erase the record.

Field
holder_nameRequired inside a credential, up to 200 characters. Printed publicly, so use a business or trading name. A value that is entirely an email address is refused. A trading name that is a handle, such as @SomePrintShop, is a name and passes.
titleUp to 80 characters. "Commercial license" when you leave it out.
registration_idYour own printed reference, up to 64 characters. Warden displays it and never looks anything up by it.
registered_atAn ISO 8601 date.
publish_termsAccepted and stored. The public page draws nothing from it today, because a creator's rules beside a holder's name read as a judgment about that holder. False when left out.
retiredWithdraws the credential itself. The page keeps resolving and says it was withdrawn. For a member who lapsed, send normalized_state: inactive and leave retired false. False when left out.

Identities

identities is where the member sells, at most 50 in a record. It is the full set your system vouches for.

You sendWhat happens to the identities you supplied before
A listEach one in the list is written. Anything you sent before and now leave out is withdrawn.
[]Every identity you supplied is withdrawn.
No identities keyNothing changes.

A sync never touches identities an admin typed into Warden by hand, or identities a member added through an enrollment invitation. To remove those as well, for a member who deleted their account, erase the record.

The verification page lists public identities only while the record reads active. A lapsed, withdrawn or unconfirmed member's shop links stay stored and are not shown. They come back when a sync says active again.

Field
sourceRequired. The platform, up to 80 characters. Warden lowercases it.
urlUp to 600 characters, and it must be http or https.
handleUp to 200 characters. Optional when there is a url.
publicWhether the verification page lists it. False when left out.
assuranceregistered, the default, for anything the member typed into a form. issuer_hosted only for a storefront the creator's own platform hosts and controls.

An identity needs a handle or a url. Two identities in one record that come to the same thing are stored once.

When you send a url and no handle, Warden reads the handle from a shop's own page, for two sources only: etsy, from an address such as etsy.com/shop/<name> or <name>.etsy.com, and ebay, from ebay.<tld>/usr/<name> or /str/<name>. Otherwise it leaves the handle empty, and the URL is still stored and shown. Only an identity with a handle, or an address Warden can compare, can link an investigation to the member.

No verified value exists for assurance, because Warden does not check who controls an account.

What is refused

Unknown fields are refused by name, on the body, on a record, on credential and on an identity. Warden has no field for an email address, a phone number, a street address, a tax id, a payment detail or a provider account id, and it will not quietly accept one.

{ "index": 3, "external_id": "member-1004", "refused": "record", "because": "warden does not accept email on a record. It accepts: external_id, status, normalized_state, tier, capabilities, effective_from, expires_at, credential, identities." }

One bad identity

An identity Warden cannot store is refused by itself. The record's state, tier, capabilities, dates and credential are still written, so a lapse always lands even when a shop address in the same record is one Warden refuses. The refusal names the record and the identity by position:

{ "index": 7, "external_id": "member-1008", "refused": "identity", "identity": 1, "because": "identity.url has to be an http or https address." }

A refused identity may have been meant to replace one you sent earlier, and Warden cannot always tell which. So the identities you supplied before on the same source stay as they are until a sync sends a set Warden can read. Identities on other sources follow the list as usual. When a refused identity has no readable source, nothing on that record is withdrawn in that request.

If identities itself cannot be read, because it is not a list or has more than 50 entries, the record is written and its stored identities are left alone, with "refused": "identities".

The response

{
  "records": [
    {
      "external_id": "member-1001",
      "public_id": "q3Zr8mW1x0aB4cD5eF6gHi",
      "verify_url": "https://3dwarden.com/verify/q3Zr8mW1x0aB4cD5eF6gHi",
      "normalized_state": "active",
      "capabilities": ["physical_sales"],
      "last_synced_at": "2026-09-21T18:04:11.123456+00:00",
      "retired": false
    }
  ],
  "rejected": [
    { "index": 3, "external_id": "member-1004", "refused": "record", "because": "credential.holder_name is shown publicly and may not be an email address." }
  ]
}

A request with a valid key and a readable body returns 200. A bad record is refused by itself and nothing of it is stored. The others are written. refused says how much was refused: record, identity or identities. Only record means the record was not written.

records holds the records that were written, in the order you sent them. A refused record is absent, so positions shift. Match on external_id.

index is the record's position in your request, whether the API or the database refused it. external_id is present unless the record had none. Log rejected and carry on.

public_id is null for a record with no credential. When there is one, store it against your member. It is minted once and never changes while Warden holds the record, through lapses, reactivations and withdrawals.

See Sync entitlements for the reference, and Errors for what a whole request can be refused with.

Plans

The API is part of the Creator and Studio plans. For an organization on a plan without it, a sync still updates the records Warden already holds. A new record comes back in rejected with "refused": "record". A new credential is not minted, so that record's public_id stays null.