Skip to Content
We are live but in Staging 🎉

Credentials — API Reference

Package: dodil.data.source.v1 · Service: SourceService

Credentials are org-scoped (not bucket-scoped) and may optionally link to a source_id. Five credential types, discriminated by CredentialType and a oneof credential_data payload — see Core Concepts → Credential.

Preview — the entire Credentials API surface, including the OAuth flow, is paired with external sources, which are Preview. The production internal-S3 path requires no credential. Shapes below reflect today’s API; expect refinements.

RPCHTTP
StoreCredentialPOST /admin/credentials
ListCredentialsGET /admin/credentials
DeleteCredentialDELETE /admin/credentials/:credential_id
GetOAuthUrlPOST /admin/oauth/authorize
ExchangeOAuthCodePOST /admin/oauth/token
RefreshOAuthTokenPOST /admin/credentials/:credential_id/refresh

Secrets are stored in HashiCorp Vault behind K3, and there is no way to read one back out. ListCredentials returns CredentialInfo only — never a secret payload. /admin/credentials/:credential_id accepts DELETE and nothing else.

Two retired RPCs

Both were removed from SourceService, and neither has a route in the router. Calls fail — do not build against them.

Retired RPCWhy it went
GetCredentialIt returned raw access/refresh tokens, API keys, service-account JSON and PATs — at VIEWER tier. A control plane whose job is to hold secrets in Vault must not offer an org-level RPC that reads them back in plaintext. There is no replacement, and none is planned: nothing legitimate needed it.
ValidateCredentialIt validated nothing. It echoed the stored is_valid — which is only ever written true — and stamped validated_at: now(), implying a live provider check it never performed. A constant-true function that reports freshness is worse than an absent one. A real probe would be a new RPC that actually calls the provider.

Because GetCredential is gone, there is no auto_refresh shortcut. Refresh expiring OAuth tokens explicitly with RefreshOAuthToken.

gRPC setup — grpcurl, endpoints, reflection, and field-name casing — is covered once in Conventions → Using gRPC.

StoreCredential

Stores a credential directly. For OAuth providers, prefer the OAuth flow below — it handles redirect + code exchange end-to-end. Use this for API keys, PATs, service-account JSON keys, and access keys.

Request

GitHub PAT variant:

curl -sS -X POST "https://api.data.dodil.io/admin/credentials" \ -H "Authorization: Bearer $DODIL_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "provider": "SOURCE_PROVIDER_GITHUB", "credentialType": "CREDENTIAL_TYPE_PAT", "displayName": "github-readonly", "isPrimary": true, "pat": { "token": "ghp_...", "username": "dodil-bot" } }'

External S3 access-key variant:

curl -sS -X POST "https://api.data.dodil.io/admin/credentials" \ -H "Authorization: Bearer $DODIL_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "provider": "SOURCE_PROVIDER_INTERNAL_S3", "credentialType": "CREDENTIAL_TYPE_ACCESS_KEY", "displayName": "external-s3", "accessKey": { "accessKeyId": "AKIA...", "secretAccessKey": "wJalrXUt...", "region": "us-east-1", "endpoint": "https://s3.example.com" } }'

Response

{ "credentialId": "cred_a1b2..." }

ListCredentials

Returns CredentialInfo rows only — no secret payloads. Filter by source_id or provider. This is the only read path for credentials.

isValid on the response is not a health signal. It is only ever written true at store time and nothing ever revalidates it. Treat it as “a credential row exists”, not “the provider still accepts this credential” — the RPC that claimed to check that (ValidateCredential) was retired precisely because it never did. To find out whether a credential still works, run a sync and read the source’s last_error via GetSyncStatus.

Request

All credentials for Google Drive — provider is the enum’s numeric value over HTTP, not its wire name:

curl -sS "https://api.data.dodil.io/admin/credentials?provider=10" \ -H "Authorization: Bearer $DODIL_TOKEN"

All credentials linked to a specific source:

curl -sS "https://api.data.dodil.io/admin/credentials?source_id=src_a1b2..." \ -H "Authorization: Bearer $DODIL_TOKEN"

The handler deserializes provider as an integer, so ?provider=SOURCE_PROVIDER_GOOGLE_DRIVE is rejected. The numbers are not contiguous:

Provider?provider=
SOURCE_PROVIDER_INTERNAL_S31
SOURCE_PROVIDER_GOOGLE_DRIVE10
SOURCE_PROVIDER_SHAREPOINT11
SOURCE_PROVIDER_CONFLUENCE12
SOURCE_PROVIDER_GITHUB20

gRPC callers pass the wire name as usual.

Response

{ "credentials": [ { "credentialId": "cred_a1b2...", "sourceId": "src_a1b2...", "provider": "SOURCE_PROVIDER_GOOGLE_DRIVE", "credentialType": "CREDENTIAL_TYPE_OAUTH2", "displayName": "gdrive-main", "isPrimary": true, "isValid": true, "createdAt": "1716840000000", "updatedAt": "1716843600000", "expiresAt": "1716847200000" } ] }

DeleteCredential

Request

curl -sS -X DELETE "https://api.data.dodil.io/admin/credentials/cred_a1b2..." \ -H "Authorization: Bearer $DODIL_TOKEN"

Response

Empty (DeleteCredentialResponse {}).

OAuth flow

Three RPCs, one per stage of the standard OAuth authorization-code flow:

  1. GetOAuthUrl — your backend asks K3 for an authorization URL; K3 returns the URL with the right scopes + client ID for the provider.
  2. User redirect — your frontend redirects the user to that URL; the user authorizes; the provider redirects back to your redirect_uri with ?code=...&state=....
  3. ExchangeOAuthCode — your backend hands the code back to K3, which exchanges it for tokens and stores them as a credential. Returns the credential_id.

Later, RefreshOAuthToken refreshes expiring tokens. This is the only refresh path — the implicit auto_refresh shortcut went with GetCredential.

GetOAuthUrl

Request

curl -sS -X POST "https://api.data.dodil.io/admin/oauth/authorize" \ -H "Authorization: Bearer $DODIL_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "provider": "SOURCE_PROVIDER_GOOGLE_DRIVE", "redirectUri": "https://app.example.com/k3/oauth/callback", "scopes": ["https://www.googleapis.com/auth/drive.readonly"], "state": "req-123" }'

Response

{ "authUrl": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...", "state": "req-123" }

ExchangeOAuthCode

Request

curl -sS -X POST "https://api.data.dodil.io/admin/oauth/token" \ -H "Authorization: Bearer $DODIL_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "provider": "SOURCE_PROVIDER_GOOGLE_DRIVE", "code": "<oauth_code>", "redirectUri": "https://app.example.com/k3/oauth/callback", "state": "req-123", "displayName": "gdrive-main" }'

Response

{ "credentialId": "cred_a1b2..." }

RefreshOAuthToken

Request

curl -sS -X POST "https://api.data.dodil.io/admin/credentials/cred_a1b2.../refresh" \ -H "Authorization: Bearer $DODIL_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "credentialId": "cred_a1b2..." }'

Response

{ "success": true, "expiresAt": "1716847200000" }

See also