Customer Onboarding
Source: docs/runbooks/customer-onboarding.md
# Customer Onboarding Runbook
Purpose: onboard a new customer with what is live today (assisted onboarding), including tenant setup, integration keys, first upload validation, and manual billing ops.
---
## Reality Check (Live Today)
| Capability | Current path |
|---|---|
| Create tenant | `POST /api/admin/tenants` (recommended) or `tenant:invite` CLI |
| Issue integration key | `POST /api/admin/tenants/:id/integration-keys` (recommended) or `POST /api/integration-keys` (tenant context) |
| Configure detectors | `POST /api/admin/tenants/:id/detectors` |
| Onboarding status API | `GET /api/admin/tenants/:id/onboarding-status` |
| Carrier account linking | Not yet exposed via `/api/admin/tenants/:id/carrier-accounts` |
Notes:
- This is an assisted onboarding flow. Public self-serve signup is out of scope.
- Billing is internal invoice lifecycle. Stripe checkout/subscriptions are not implemented.
---
## Prerequisites
- API deployed and healthy (`GET /health` returns `ok` or `degraded`)
- Admin auth path available (Clerk admin session or admin-scoped bearer token)
- `KEY_HASH_SECRET` configured in API runtime (required for integration-key auth)
- Customer agreement signed and module scope confirmed (`ship`, `trade`, or both)
---
## Assisted Onboarding Flow (Recommended)
### 1. Create Tenant (Admin API)
```bash
curl -X POST {{BASE_URL}}/api/admin/tenants \
-H "Authorization: Bearer <admin-jwt>" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp",
"slug": "acme-corp",
"modules": ["ship", "trade"],
"clerkOrgId": "org_123",
"primaryContact": { "email": "ops@acme.com", "role": "org:admin" }
}'
```
Returns tenant metadata plus `inviteStatus`.
Fallback (CLI):
```bash
pnpm --filter @rgl8r/api tenant:invite -- \
--name "Acme Corp" \
--email "ops@acme.com" \
--role "org:admin"
```
### 2. Create Integration Key (Admin API)
```bash
curl -X POST {{BASE_URL}}/api/admin/tenants/{{TENANT_ID}}/integration-keys \
-H "Authorization: Bearer <admin-jwt>" \
-H "Content-Type: application/json" \
-d '{
"name": "Production CLI",
"scopes": ["ship:upload", "ship:read", "jobs:read", "trade:upload", "trade:read"],
"allowedAdapters": ["generic-shipment-csv", "catalog-excel"],
"expiresAt": null
}'
```
Important:
- Secret is returned once.
- Rotation endpoint: `POST /api/integration-keys/:id/rotate` (tenant-scoped route).
### 3. Configure Detector Overrides (Optional)
```bash
curl -X POST {{BASE_URL}}/api/admin/tenants/{{TENANT_ID}}/detectors \
-H "Authorization: Bearer <admin-jwt>" \
-H "Content-Type: application/json" \
-d '{
"detectors": [
{ "detectorCode": "AMOUNT_VARIANCE", "enabled": true },
{ "detectorCode": "SIMA_EXPOSURE", "enabled": true }
]
}'
```
If you skip this step, module defaults apply.
### 4. Verify Onboarding Status
```bash
curl -H "Authorization: Bearer <admin-jwt>" \
"{{BASE_URL}}/api/admin/tenants/{{TENANT_ID}}/onboarding-status"
```
---
## Customer Validation Flow
### 1. Exchange integration key for JWT
```bash
curl -X POST {{BASE_URL}}/api/auth/token/integration \
-H "x-api-key: <integration-secret>"
```
Use returned `access_token` as `Authorization: Bearer <token>`.
### 2. Upload test data
SHIP upload:
```bash
curl -X POST {{BASE_URL}}/api/ship/upload \
-H "Authorization: Bearer <customer-jwt>" \
-F "file=@/path/to/test-shipments.csv"
```
TRADE upload:
```bash
curl -X POST {{BASE_URL}}/api/upload \
-H "Authorization: Bearer <customer-jwt>" \
-F "file=@/path/to/test-data.xlsx"
```
Optional TRADE authority selection:
- Add `screeningAuthority=US` form field for US AD/CVD screening.
### 3. Validate outputs
```bash
curl -H "Authorization: Bearer <customer-jwt>" "{{BASE_URL}}/api/jobs/{{JOB_ID}}"
curl -H "Authorization: Bearer <customer-jwt>" "{{BASE_URL}}/api/ship/findings?limit=20"
curl -H "Authorization: Bearer <customer-jwt>" "{{BASE_URL}}/api/sima/results?limit=20"
```
---
## Go-Live + Billing Ops
1. Customer starts production uploads.
2. Monitor first 24 hours for ingest failures and output quality.
3. Schedule one-week review.
4. Billing (manual invoice lifecycle):
- Generate: `POST /api/admin/billing/invoices/generate`
- Issue: `POST /api/admin/billing/invoices/:id/issue`
- Mark paid: `POST /api/admin/billing/invoices/:id/mark-paid`
- Cancel: `POST /api/admin/billing/invoices/:id/cancel`
Billing note:
- This is internal invoice workflow, not Stripe automation.
- Payment collection and reconciliation are currently handled outside the API.
---
## Checklist
### Pre-Onboarding
- [ ] Agreement signed
- [ ] Module scope confirmed
- [ ] Admin auth and key material verified (`KEY_HASH_SECRET` present)
### Setup
- [ ] Tenant created
- [ ] Integration key issued and stored by customer
- [ ] Detector configuration confirmed (defaults or overrides)
### Validation
- [ ] Token exchange succeeds (`/api/auth/token/integration`)
- [ ] First upload completes
- [ ] Findings/results reviewed with customer
### Go-Live
- [ ] Production uploads started
- [ ] 24-hour monitoring completed
- [ ] 1-week review scheduled
- [ ] Billing runbook executed if billing is active
---
## Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
| `401` on upload | JWT expired/invalid or integration key revoked | Re-exchange via `/api/auth/token/integration`; rotate key if needed |
| `403` on upload | Missing required scopes | Reissue key with correct scopes |
| `401` on `/api/auth/token/integration` with valid-looking key | Key revoked/expired, or `KEY_HASH_SECRET` missing/misconfigured (non-prod) | Verify key status; verify `KEY_HASH_SECRET`. In production, missing `KEY_HASH_SECRET` prevents API startup. |
| Upload accepted but no findings | Clean data or detectors disabled | Verify sample data and detector config rows |
| Billing generate fails `400` | Missing required body fields | Include `tenantId`, `periodStart`, `periodEnd` |
---
## Related Docs
- [Assisted Public Launch Checklist](./assisted-public-launch-checklist.md)
- [Build & Deploy](../BUILD_AND_DEPLOY.md)
- [Integration Keys Contract](../integration-keys.md)
- [Pilot Validation Runbook](../PILOT_VALIDATION_RUNBOOK.md)