Signup Protection Pricing Documentation Sign up Log in

Gates

Migrate from v0 to v1

Integrations built during the early beta call POST /v0/gates/{gate_id}/decisions. The current endpoint is POST /v1/gates/{gate_id}/decisions: the same decision, returned in a flatter shape.

/v0 keeps working indefinitely, so nothing forces this migration. Migrating is a matter of changing the version segment in the URL and updating where your code reads a few fields; the request body, authentication, credit cost, and the decisions themselves are identical on both paths. The same input gets the same verdict from both versions.

What changed

/v0 wraps the verdict in a decision object and the request metadata in a meta object:

{
  "input": {
    "email": "[email protected]",
    "ip": "1.1.1.1"
  },
  "decision": {
    "action": "allow",
    "matched_rule": {
      "id": "01k1hxqrn2f7xfreq20y0p012d",
      "name": "Allow professional email providers",
      "message": null
    }
  },
  "signals": {
    "email": { ... },
    "domain": { ... },
    "ip": { ... }
  },
  "meta": {
    "version": "0.1",
    "request_id": "01K1SKM0ZDCEEES5QA9N3M5EPX",
    "duration_ms": 52.62,
    "created_at": "2025-08-04T03:57:18+00:00"
  }
}

/v1 returns the same decision as a flat object, the verdict at the top:

{
  "id": "01K1SKM0ZDCEEES5QA9N3M5EPX",
  "gate_id": "01k2wjgq8rnv6t0aefy3s7xdhb",
  "action": "allow",
  "rule": {
    "id": "01k1hxqrn2f7xfreq20y0p012d",
    "name": "Allow professional email providers",
    "message": null
  },
  "input": {
    "email": "[email protected]",
    "ip": "1.1.1.1"
  },
  "signals": {
    "email": { ... },
    "domain": { ... },
    "ip": { ... }
  },
  "duration_ms": 52.62,
  "created_at": "2025-08-04T03:57:18+00:00"
}

Field by field:

/v0 /v1
decision.action action
decision.matched_rule rule
meta.request_id id
meta.duration_ms duration_ms
meta.created_at created_at
meta.version Removed. The URL carries the version.
Not returned gate_id
input, signals Unchanged, same content and position in your parsing.

Two of these are more than a rename:

  • rule is always present. /v0 omits matched_rule when no rule matched and the gate's default_action applied. /v1 always returns the rule key, null in that case. Code that checks whether matched_rule exists becomes a check for rule not being null.
  • id is the decision's handle. It carries the value /v0 reported as meta.request_id, and it is the {decision_id} the Management API accepts to read the decision back later.

Error handling changes in these cases:

Case /v0 /v1
Invalid or missing input: missing_input, invalid_email, invalid_domain, both_email_and_domain_provided 400 422
Application throughput limit exceeded 429 with error: "Too many requests" 429 rate_limit_exceeded
Monthly credits used up 429 rate_limit_exceeded 429 quota_exceeded

Errors covers the /v1 contract in full.

Update your integration

For successful decision responses, change /v0/ to /v1/ in the URL and move the field reads. Handle failures using the error-handling policy before reading the decision body:

  response = requests.post(
-     f"https://api.usercheck.com/v0/gates/{GATE_ID}/decisions",
+     f"https://api.usercheck.com/v1/gates/{GATE_ID}/decisions",
      headers={
          "Authorization": f"Bearer {API_KEY}",
          "Content-Type": "application/json",
      },
      json={"email": email},
  )
  data = response.json()
- decision = data.get("decision", {})

- if decision.get("action") == "block":
-     message = decision.get("matched_rule", {}).get("message")
+ if data.get("action") == "block":
+     message = (data.get("rule") or {}).get("message")
      raise SignUpError(message)

- request_id = data.get("meta", {}).get("request_id")
+ request_id = data.get("id")

Note the guard on the rule: /v0 omits matched_rule entirely when the default action applied, so a .get() default was enough. /v1 always includes rule but sets it to null in that case, so the fallback becomes or {}.

Everything your code reads from signals stays exactly where it was.

For errors, use the status and code changes in the table above. On /v1, rate_limit_exceeded means pause and retry, while quota_exceeded means check your credits. Honor Retry-After when present and use bounded backoff when it is absent.

Cloudflare IP blocks return 429 rate_limit_exceeded on both versions. Since /v0 also uses that code for exhausted credits, check your account usage if the cause is unclear. Do not infer quota exhaustion from a missing header.

Both versions serve the same gates, so you can migrate one caller at a time: a service still on /v0 and one already on /v1 can evaluate the same gate side by side while you roll the change out.

Previous
Migrate from Email & Domain