Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Access Grants

Access grants are per-domain delegation with optional record-level restrictions. Use them when role assignments are too coarse — you want to give a specific team access to a specific zone, limited to certain record types or name patterns, optionally for a bounded time.

Base path: /api/v1/domains/{domain_id}/access-grants Required permission: access_grants:* on the domain (typically domain admin or tenant admin)

When to use grants vs role assignments#

Situation Use
Team always has full control of the tenant Tenant admin role
Team always has full control of one zone Domain admin role (scoped to the zone)
Contractor needs access to one zone for Q4 only Access grant with expires_at
Team should only edit *.dev. records Access grant with record_pattern
Service only needs to manipulate A and AAAA Access grant with record_types

Access grants are additive to role assignments — a user with no tenant role can still manage a domain they have a grant on.

Shape#

An access grant consists of:

  • Grant type. user or group.
  • Grantee ID. The user or group receiving access.
  • Role. What level of access they get on this domain (domain_manager, record_editor, read_only, or a custom role).
  • Record pattern. Optional. Glob-style pattern on record name (*.dev, api.*).
  • Record types. Optional. List of types the grant applies to (["A", "AAAA", "CNAME"]).
  • Expires at. Optional. Grant auto-expires at this timestamp.
  • Notes. Free-form.

List grants#

bash
1
2
curl https://scaidns.scailabs.ai/api/v1/domains/$DOMAIN_ID/access-grants \
  -H "X-API-Key: $SCAIDNS_API_KEY"

Query parameters:

Param Type Notes
include_expired boolean Include grants past their expires_at

Response:

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "data": [
    {
      "id": "ag_abc123",
      "domain_id": "d_xyz",
      "grant_type": "user",
      "grantee_id": "u_contractor",
      "grantee_name": "Contractor Name",
      "grantee_email": "contractor@example.com",
      "role_id": "r_record_editor",
      "role_name": "record_editor",
      "record_pattern": "*.staging",
      "record_types": ["A", "AAAA", "CNAME"],
      "expires_at": "2026-12-31T23:59:59Z",
      "notes": "Q4 staging delegation",
      "created_at": "2026-04-01T10:00:00Z"
    }
  ],
  "total": 3,
  "domain_id": "d_xyz"
}

Create a grant#

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
curl -X POST https://scaidns.scailabs.ai/api/v1/domains/$DOMAIN_ID/access-grants \
  -H "X-API-Key: $SCAIDNS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "user",
    "grantee_id": "u_contractor",
    "role_id": "r_record_editor",
    "record_pattern": "*.staging",
    "record_types": ["A", "AAAA", "CNAME"],
    "expires_at": "2026-12-31T23:59:59Z",
    "notes": "Q4 staging delegation"
  }'
python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
resp = httpx.post(
    f"https://scaidns.scailabs.ai/api/v1/domains/{DOMAIN_ID}/access-grants",
    headers={"X-API-Key": os.environ["SCAIDNS_API_KEY"]},
    json={
        "grant_type": "user",
        "grantee_id": "u_contractor",
        "role_id": "r_record_editor",
        "record_pattern": "*.staging",
        "record_types": ["A", "AAAA", "CNAME"],
        "expires_at": "2026-12-31T23:59:59Z",
        "notes": "Q4 staging delegation",
    },
)
typescript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
await fetch(
  `https://scaidns.scailabs.ai/api/v1/domains/${domainId}/access-grants`,
  {
    method: "POST",
    headers: {
      "X-API-Key": process.env.SCAIDNS_API_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      grant_type: "user",
      grantee_id: "u_contractor",
      role_id: "r_record_editor",
      record_pattern: "*.staging",
      record_types: ["A", "AAAA", "CNAME"],
      expires_at: "2026-12-31T23:59:59Z",
      notes: "Q4 staging delegation",
    }),
  },
);

Get, update, delete#

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Get
curl https://scaidns.scailabs.ai/api/v1/domains/$DOMAIN_ID/access-grants/$GRANT_ID \
  -H "X-API-Key: $SCAIDNS_API_KEY"

# Update (change role, pattern, types, expiry, notes)
curl -X PATCH https://scaidns.scailabs.ai/api/v1/domains/$DOMAIN_ID/access-grants/$GRANT_ID \
  -H "X-API-Key: $SCAIDNS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"expires_at": "2027-06-30T00:00:00Z"}'

# Revoke
curl -X DELETE https://scaidns.scailabs.ai/api/v1/domains/$DOMAIN_ID/access-grants/$GRANT_ID \
  -H "X-API-Key: $SCAIDNS_API_KEY"

How patterns match#

Record patterns are glob-style. Only * is supported (no ? or character classes):

Pattern Matches
* Anything
*.staging foo.staging, bar.staging.x, not staging alone
api.* api.foo, api.bar.baz, not api alone
web* web, web1, website, webapi.foo
example.com Exact match

Patterns match against the record name as ScaiDNS stores it (typically the relative form — www for www.example.com).

How record types filter#

If record_types is present, the grant only applies when the caller is mutating those types. Reading remains unrestricted (read access is either granted or not — you can't have read-only-A).

Empty or omitted record_types means all types.

Grant precedence#

When a user has multiple paths to a permission:

  1. Platform admin always wins.
  2. Tenant admin wins for their tenant.
  3. Role assignments scoped to a domain (including grants) are merged.
  4. If any grant matches, access is allowed. If none match, access is denied.

Grants are strictly additive. A grant can't reduce someone's existing permissions — if they're already a tenant admin, a restrictive grant doesn't limit them.

Groups#

Grants can target groups. All members of the group inherit the grant.

json
1
2
3
4
5
{
  "grant_type": "group",
  "grantee_id": "g_devops",
  "role_id": "r_record_editor"
}

When a user is added to the group, the grant applies to them. When removed, it no longer does.

Expiry#

Grants with expires_at in the past are ignored when computing permissions. They're not automatically deleted — use GET /access-grants/?include_expired=false (the default) to hide them, or clean them up periodically.

Audit#

Every grant creation, update, and revocation goes to the audit log with action access_grant.*. See Audit Log.

Patterns#

"Contractor access until end of engagement"#

json
1
2
3
4
5
6
7
{
  "grant_type": "user",
  "grantee_id": "u_contractor",
  "role_id": "r_domain_manager",
  "expires_at": "2026-06-30T23:59:59Z",
  "notes": "Ends with engagement"
}

"Team can only manage dev subdomains"#

json
1
2
3
4
5
6
7
{
  "grant_type": "group",
  "grantee_id": "g_dev_team",
  "role_id": "r_record_editor",
  "record_pattern": "*.dev",
  "notes": "Dev-only for this team"
}

"Service manipulates only A records for load balancing"#

json
1
2
3
4
5
6
7
8
{
  "grant_type": "user",
  "grantee_id": "u_lb_service",
  "role_id": "r_record_editor",
  "record_types": ["A", "AAAA"],
  "record_pattern": "lb-*",
  "notes": "LB IP rotation service"
}

Errors#

Status Meaning
400 Invalid pattern, unknown record type, malformed expiry
403 Caller can't manage grants on this domain
404 Grant, grantee, or role not found
409 Duplicate grant (same grantee + role already exists)
422 Trying to grant a role with permissions the caller doesn't have
Updated 2026-05-17 02:38:20 View source (.md) rev 1