Summary and recommendation
Stripe exposes two entirely separate user concepts that must not be conflated in an identity graph: Customers (`/v1/customers`) are payment entities managed via the standard REST API; Dashboard Team Members are internal users managed via the SCIM 2.0 endpoint (`https://dashboard.stripe.com/scim/v2`), which requires an enterprise agreement and active SSO.
Mixing these two systems in a provisioning pipeline is a common and consequential architectural error.
Authentication uses HTTP Basic Auth with a secret API key (key as username, empty password) or a Bearer token via the Authorization header. For least-privilege integrations, use Restricted Keys scoped to specific resources (e.g., `customers:read`, `customers:write`) rather than a full secret key.
Live mode enforces 100 read req/s and 100 write req/s per key; HTTP 429 responses require exponential backoff. Rate limit state is surfaced via `Stripe-Ratelimit-Limit` and `Stripe-Ratelimit-Remaining` response headers.
API quick reference
| Has user API | Yes |
| Auth method | HTTP Basic Auth with secret API key (key as username, empty password); Bearer token also accepted via Authorization header |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | Enterprise (requires SSO enabled via enterprise agreement; private preview as of last known documentation) |
Authentication
Auth method: HTTP Basic Auth with secret API key (key as username, empty password); Bearer token also accepted via Authorization header
Setup steps
- Log in to the Stripe Dashboard and navigate to Developers → API keys.
- Copy the Secret key (sk_live_... or sk_test_... for test mode).
- Pass the key as the username in HTTP Basic Auth:
Authorization: Basic base64(sk_live_xxx:)or asAuthorization: Bearer sk_live_xxx. - For restricted access, create a Restricted Key with only the required permissions scoped to specific resources.
- For OAuth-connected platforms, obtain an access token via Stripe Connect OAuth 2.0 flow (authorize endpoint: https://connect.stripe.com/oauth/authorize).
Required scopes
| Scope | Description | Required for |
|---|---|---|
| read_write (full secret key) | Full read/write access to all API resources on the account. | Creating, updating, and deleting customers and other resources. |
| Restricted Key: customers:read | Read-only access to Customer objects. | Listing and retrieving customers. |
| Restricted Key: customers:write | Write access to Customer objects. | Creating and updating customers. |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | string | Unique identifier for the Customer object (cus_...). | auto-generated | immutable | Used as cursor for pagination. |
| string | Customer's email address. | optional | updatable | Not required to be unique. | |
| name | string | Customer's full name. | optional | updatable | |
| phone | string | Customer's phone number. | optional | updatable | |
| description | string | Arbitrary description for internal reference. | optional | updatable | |
| metadata | object | Key-value pairs for storing additional structured data. | optional | updatable | Max 50 keys; key max 40 chars; value max 500 chars. |
| address | object | Customer's billing address (line1, line2, city, state, postal_code, country). | optional | updatable | |
| shipping | object | Shipping address and contact info. | optional | updatable | |
| balance | integer | Current credit/debit balance in smallest currency unit. | optional | updatable | Negative = credit; positive = amount owed. |
| currency | string | Three-letter ISO currency code for the customer's default currency. | optional | updatable | Set on first invoice or explicitly. |
| default_source | string | ID of the default payment source attached to the customer. | optional | updatable | Deprecated in favor of default_payment_method on subscriptions. |
| invoice_prefix | string | Prefix for invoice numbers generated for this customer. | optional | updatable | |
| invoice_settings | object | Default invoice settings including default_payment_method and footer. | optional | updatable | |
| tax_exempt | string | Tax exemption status: none, exempt, or reverse. | optional | updatable | |
| created | timestamp | Unix timestamp of when the customer was created. | auto-generated | immutable | |
| livemode | boolean | Whether the object exists in live mode or test mode. | auto-set | immutable | |
| deleted | boolean | Present and true if the customer has been deleted. | n/a | set on delete | Deleted customers cannot be recovered via API. |
Core endpoints
Create Customer
- Method: POST
- URL:
https://api.stripe.com/v1/customers - Watch out for: Email uniqueness is NOT enforced; duplicate emails create separate customer objects.
Request example
curl https://api.stripe.com/v1/customers \
-u sk_test_xxx: \
-d email='user@example.com' \
-d name='Jane Doe' \
-d metadata[internal_id]='usr_001'
Response example
{
"id": "cus_ABC123",
"object": "customer",
"email": "user@example.com",
"name": "Jane Doe",
"created": 1700000000,
"livemode": false
}
Retrieve Customer
- Method: GET
- URL:
https://api.stripe.com/v1/customers/{id} - Watch out for: Retrieving a deleted customer returns the object with
deleted: truerather than a 404.
Request example
curl https://api.stripe.com/v1/customers/cus_ABC123 \
-u sk_test_xxx:
Response example
{
"id": "cus_ABC123",
"object": "customer",
"email": "user@example.com",
"name": "Jane Doe",
"deleted": false
}
Update Customer
- Method: POST
- URL:
https://api.stripe.com/v1/customers/{id} - Watch out for: Stripe uses POST (not PATCH) for updates. Passing
metadata=''clears all metadata keys.
Request example
curl https://api.stripe.com/v1/customers/cus_ABC123 \
-u sk_test_xxx: \
-d email='new@example.com' \
-d 'metadata[status]=active'
Response example
{
"id": "cus_ABC123",
"email": "new@example.com",
"metadata": {"status": "active"}
}
Delete Customer
- Method: DELETE
- URL:
https://api.stripe.com/v1/customers/{id} - Watch out for: Deletion is permanent and irreversible. Active subscriptions are cancelled immediately.
Request example
curl -X DELETE https://api.stripe.com/v1/customers/cus_ABC123 \
-u sk_test_xxx:
Response example
{
"id": "cus_ABC123",
"object": "customer",
"deleted": true
}
List Customers
- Method: GET
- URL:
https://api.stripe.com/v1/customers - Watch out for: Filter by email using
?email=user@example.combut this is an exact match and case-sensitive.
Request example
curl 'https://api.stripe.com/v1/customers?limit=10&starting_after=cus_XYZ' \
-u sk_test_xxx:
Response example
{
"object": "list",
"data": [{"id": "cus_ABC123", ...}],
"has_more": true,
"url": "/v1/customers"
}
Search Customers
- Method: GET
- URL:
https://api.stripe.com/v1/customers/search - Watch out for: Search uses a different pagination model (
next_pagetoken, not cursor IDs). Indexed with a delay; not real-time.
Request example
curl 'https://api.stripe.com/v1/customers/search?query=email%3A%27user%40example.com%27' \
-u sk_test_xxx:
Response example
{
"object": "search_result",
"data": [{"id": "cus_ABC123", ...}],
"has_more": false,
"next_page": null
}
Create Portal Session (self-service)
- Method: POST
- URL:
https://api.stripe.com/v1/billing_portal/sessions - Watch out for: Portal configuration must be created in the Dashboard first. Session URLs expire after use or after a short TTL.
Request example
curl https://api.stripe.com/v1/billing_portal/sessions \
-u sk_test_xxx: \
-d customer=cus_ABC123 \
-d return_url='https://example.com/account'
Response example
{
"id": "bps_xxx",
"object": "billing_portal.session",
"url": "https://billing.stripe.com/session/xxx"
}
Retrieve Dashboard User (Team Member) - via SCIM
- Method: GET
- URL:
https://dashboard.stripe.com/scim/v2/Users/{id} - Watch out for: SCIM endpoint is separate from the REST API. Requires SSO enabled and enterprise agreement. Roles are managed via SAML attribute statements, not SCIM.
Request example
curl https://dashboard.stripe.com/scim/v2/Users/usr_xxx \
-H 'Authorization: Bearer <scim_token>'
Response example
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "usr_xxx",
"userName": "user@example.com",
"active": true
}
Rate limits, pagination, and events
- Rate limits: Stripe enforces rate limits per secret API key. Live mode allows 100 read requests/second and 100 write requests/second by default. Test mode limits are lower. Limits can be raised by contacting Stripe support.
- Rate-limit headers: Yes
- Retry-After header: No
- Rate-limit notes: HTTP 429 is returned when rate limit is exceeded. Stripe recommends exponential backoff. Headers
Stripe-Ratelimit-LimitandStripe-Ratelimit-Remainingare returned on responses. - Pagination method: cursor
- Default page size: 10
- Max page size: 100
- Pagination pointer: starting_after / ending_before (cursor-based using object ID);
limitcontrols page size
| Plan | Limit | Concurrent |
|---|---|---|
| Standard (live mode) | 100 read req/s, 100 write req/s | 0 |
| Standard (test mode) | 25 read req/s, 25 write req/s | 0 |
- Webhooks available: Yes
- Webhook notes: Stripe sends webhook events to a configured HTTPS endpoint when objects change. Configure endpoints in Dashboard → Developers → Webhooks or via the Webhook Endpoints API.
- Alternative event strategy: Stripe provides a CLI (
stripe listen) for local webhook testing. Polling the List Customers endpoint withcreated[gte]filter is an alternative for batch sync. - Webhook events: customer.created, customer.updated, customer.deleted, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, customer.source.created, customer.source.deleted, billing_portal.session.created
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: Enterprise (requires SSO enabled via enterprise agreement; private preview as of last known documentation)
Endpoint: https://dashboard.stripe.com/scim/v2
Supported operations: GET /Users (list team members), GET /Users/{id} (retrieve team member), POST /Users (provision team member), PUT /Users/{id} (update team member), PATCH /Users/{id} (partial update / deactivate), DELETE /Users/{id} (deprovision team member)
Limitations:
- SCIM manages Stripe Dashboard team members only, not payment Customers.
- Role assignment is controlled via SAML attribute statements (e.g., from Okta/Entra), not SCIM attributes.
- Requires SSO to be configured and active before SCIM can be enabled.
- SCIM provisioning was in private preview; general availability status should be confirmed with Stripe.
- Only Okta and Microsoft Entra ID (Azure AD) are documented as supported IdPs for SCIM.
- Google Workspace and OneLogin are not documented as supported for SCIM.
Common scenarios
Three scenarios cover the primary integration patterns. First, provisioning a paying customer: POST to /v1/customers with email, name, and metadata, then attach a PaymentMethod via /v1/payment_methods/{pm_id}/attach - attachment does not automatically set the method as default.
a follow-up POST to /v1/customers/{id} with invoice_settings[default_payment_method] is required. Never pass raw card numbers server-side; tokenize via Stripe.
js.
Second, syncing Dashboard team members via SCIM (Okta): configure the SCIM base URL (https://dashboard.stripe.com/scim/v2) and bearer token in Okta, then set Stripe roles exclusively via SAML attribute statements - SCIM attributes cannot control role assignment. Deprovisioning sends PATCH /scim/v2/Users/{id} with active=false; full record deletion behavior should be verified against your enterprise agreement terms.
Third, bulk customer export: paginate GET /v1/customers?limit=100 using starting_after cursor (last object ID) until has_more=false. The Search endpoint (/v1/customers/search) uses a separate next_page token pagination model and has indexing latency - it is not suitable for real-time lookups of recently created records.
Provision a new paying customer and attach a payment method
- POST /v1/customers with email, name, and metadata fields to create the Customer object.
- Use Stripe.js / Payment Element on the frontend to collect card details and create a PaymentMethod (pm_...).
- POST /v1/payment_methods/{pm_id}/attach with customer=cus_... to attach the method.
- POST /v1/customers/{id} with invoice_settings[default_payment_method]=pm_... to set as default.
- Listen for customer.created and customer.updated webhooks to sync to your database.
Watch out for: Never pass raw card numbers to your server; always tokenize via Stripe.js. Attaching a PaymentMethod does not automatically set it as default.
Sync Stripe Dashboard team members via SCIM (Okta)
- Confirm enterprise agreement with Stripe and enable SSO (SAML 2.0) via Dashboard → Settings → Team and security.
- In Okta, add the Stripe SCIM application and configure the SCIM base URL: https://dashboard.stripe.com/scim/v2.
- Generate a SCIM bearer token in the Stripe Dashboard and enter it in Okta as the API token.
- Configure SAML attribute statements in Okta to pass the desired Stripe role (e.g., developer, analyst) as a SAML attribute.
- Assign users in Okta; Okta will POST /scim/v2/Users to provision them in Stripe Dashboard.
- Deprovisioning in Okta sends PATCH /scim/v2/Users/{id} with active=false to deactivate the team member.
Watch out for: Roles cannot be set via SCIM attributes; they must be configured via SAML attribute statements. Removing a user from Okta deactivates but may not fully delete the Stripe team member record.
Bulk export and reconcile all customers
- GET /v1/customers?limit=100 to fetch the first page.
- Use the last object's id as starting_after in the next request; repeat until has_more=false.
- Alternatively, use GET /v1/customers/search with a query filter (e.g., metadata['status']:'active') for filtered exports.
- Store the customer id and updated timestamp; use customer.updated webhook events to keep the local copy current.
Watch out for: The list endpoint returns up to 100 objects per page maximum. Search results have indexing lag; do not rely on search for immediately-created records. Large exports may hit rate limits; implement exponential backoff on HTTP 429.
Why building this yourself is a trap
The most operationally dangerous assumption is that removing a Dashboard team member revokes their API access. It does not. API keys issued under a Developer-role account remain valid until explicitly rotated or revoked under Developers → API keys - this is a separate, manual action with no automated trigger on team member removal.
A second structural trap relevant to any identity graph: Customer email is not a unique key. POST /v1/customers with a duplicate email creates a new, distinct Customer object rather than returning or updating the existing one.
Any identity graph that uses email as a deduplication key against Stripe Customer IDs will produce fan-out without explicit pre-flight lookup via GET /v1/customers?email=. The DELETE /v1/customers/{id} operation is permanent and immediately cancels all active subscriptions - there is no soft-delete or recovery path.
For platforms using Stripe Connect, Connected Accounts are a third user concept managed via /v1/accounts, entirely separate from both Customers and SCIM-managed Team Members. An identity graph that does not model all three object types distinctly will produce incorrect access state.
SCIM availability remains in private preview as of the last documented state; confirm general availability directly with Stripe before building a production provisioning dependency on it.
Automate Stripe workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.