Summary
POST /v2/invites/bulk works correctly with an OAT-issued JWT. However, POST /v2/invites/{id}/resend rejects the same token with a 403 error, even though the OAT has scope-invite-edit configured — the scope that exists specifically for invite management.
Token Policy Matrix (observed)
| Endpoint |
OAT JWT |
POST /v2/invites/bulk |
✅ 200 OK |
POST /v2/invites/{id}/resend |
❌ 403 Forbidden |
No token-type restriction is documented in the API reference for either endpoint.
Environment
- Docker Hub organization account (Business/Team plan)
- Caller role: Organization Owner
- OAT scopes:
scope-member-edit, scope-invite-edit, scope-group-edit, scope-activity-read
Steps to Reproduce
Step 1 — Auth with OAT
export TOKEN=$(curl -s -X POST https://hub.docker.com/v2/users/login \
-H "Content-Type: application/json" \
-d '{"username": "<org-name>", "password": "<OAT>"}' \
| jq -r '.token')
Step 2 — Bulk invite with OAT JWT — works ✅
curl -s -X POST https://hub.docker.com/v2/invites/bulk \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"org": "<org-name>", "role": "member", "invitees": [{"email": "test@example.com"}]}'
Step 3 — Get the pending invite ID
curl -s "https://hub.docker.com/v2/orgs/<org-name>/invites" \
-H "Authorization: Bearer $TOKEN" | jq '.results[0].id'
Step 4 — Resend with same OAT JWT — fails ❌
curl -s -X POST "https://hub.docker.com/v2/invites/<invite-id>/resend" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json"
Actual Response
{
"message": "token issued from organization access token is not allowed",
"errinfo": {}
}
Expected Response
{
"id": "<invite-id>",
"status": "pending"
}
Issues
- Inconsistent policy between sibling endpoints —
/v2/invites/bulk and /v2/invites/{id}/resend are under the same namespace but apply different token policies with no documentation explaining the difference.
scope-invite-edit does not cover resend — This scope exists for invite management but does not work on the resend endpoint, making it misleading and incomplete.
- No documented restriction — The API reference only states
bearerAuth is required, with no mention of OAT restrictions.
Impact
Automated invite workflows using OATs — the token type Docker recommends for org-level automation — can send invites but cannot resend them. This makes scope-invite-edit only partially functional and forces a suboptimal workaround.
Workaround
Delete the pending invite via DELETE /v2/invites/{id} and re-issue via POST /v2/invites/bulk. This triggers a new invite email rather than a true resend — functionally similar but semantically different and potentially confusing to the invitee.
References
Summary
POST /v2/invites/bulkworks correctly with an OAT-issued JWT. However,POST /v2/invites/{id}/resendrejects the same token with a 403 error, even though the OAT hasscope-invite-editconfigured — the scope that exists specifically for invite management.Token Policy Matrix (observed)
POST /v2/invites/bulkPOST /v2/invites/{id}/resendNo token-type restriction is documented in the API reference for either endpoint.
Environment
scope-member-edit,scope-invite-edit,scope-group-edit,scope-activity-readSteps to Reproduce
Step 1 — Auth with OAT
Step 2 — Bulk invite with OAT JWT — works ✅
Step 3 — Get the pending invite ID
Step 4 — Resend with same OAT JWT — fails ❌
Actual Response
{ "message": "token issued from organization access token is not allowed", "errinfo": {} }Expected Response
{ "id": "<invite-id>", "status": "pending" }Issues
/v2/invites/bulkand/v2/invites/{id}/resendare under the same namespace but apply different token policies with no documentation explaining the difference.scope-invite-editdoes not cover resend — This scope exists for invite management but does not work on the resend endpoint, making it misleading and incomplete.bearerAuthis required, with no mention of OAT restrictions.Impact
Automated invite workflows using OATs — the token type Docker recommends for org-level automation — can send invites but cannot resend them. This makes
scope-invite-editonly partially functional and forces a suboptimal workaround.Workaround
Delete the pending invite via
DELETE /v2/invites/{id}and re-issue viaPOST /v2/invites/bulk. This triggers a new invite email rather than a true resend — functionally similar but semantically different and potentially confusing to the invitee.References