> ## Documentation Index
> Fetch the complete documentation index at: https://docs.edpire.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing

> How to test your Edpire integration thoroughly before going to production.

## Test with real assessments

Create and publish assessments in your Edpire dashboard before testing your integration.
Your API key gives access to the same assessments your learners will see — no separate test environment needed.

```bash theme={null}
# .env.local
EDPIRE_API_KEY=edp_live_<your-api-key>
```

## Keeping test data out of your real numbers

Edpire does not run a separate sandbox environment, and there is no `edp_test_` key type. Test submissions are real submissions, so if you send them into your production organization they land in your analytics and your usage counters.

**Create a second organization for development.** It is the recommended pattern and it costs nothing:

1. Sign up a second organization (for example "Acme — Sandbox")
2. Create its own API key under **Integrations**
3. Register your `localhost` and staging domains under **Integrations → Security**
4. Copy or rebuild the handful of assessments you need to test against

Because quotas, analytics and active-learner counts are all scoped per organization, nothing your developers do can distort your real reporting or your bill. When you go live, swap the API key and the webhook URL. Nothing else changes.

<Note>
  **Load testing and attack simulation are different** and need our involvement. Do not run them
  against production. Request a pen test window as described in
  [Security Posture](/enterprise/security-posture), at least 5 business days ahead.
</Note>

## Test the complete flow end-to-end

Before going live, run through the full learner journey with your chosen integration pattern:

<AccordionGroup>
  <Accordion title="Hosted Redirect">
    1. Build the share URL with your assessment's share code
    2. Open it in a private browser window (to simulate a fresh learner session)
    3. Complete the assessment
    4. Verify the redirect lands on your `return_url` with `submission_id`, `score`, and `max_score` appended
    5. Fetch the submission via `GET /api/v1/submissions/{id}` and confirm the response shape matches your data model
  </Accordion>

  <Accordion title="Embedded Player">
    1. Mint an embed token from your server via `client.mintEmbedToken()`
    2. Call `EdpireAssessment.mount({ token, container })` in the browser
    3. Complete the assessment — verify the player mounts, renders questions, and accepts answers
    4. Submit and confirm `onComplete` fires with the correct `result` shape
    5. Verify per-question feedback is shown inline after submission
  </Accordion>

  <Accordion title="Custom Flow">
    1. Fetch an assessment through your backend proxy and flatten with `flattenAssessment()`
    2. Render each step with your custom UI using `EdpireQuestion` or `renderQuestion`
    3. After each answer, call your `/check` proxy — verify `feedback` shape matches the question type
    4. Confirm `correct: true` feedback is returned for known-correct answers
    5. After all questions, submit via `client.submit()` and verify the full submission record
  </Accordion>
</AccordionGroup>

## Test webhooks locally

Your local server isn't reachable from Edpire's servers directly. Use [ngrok](https://ngrok.com) or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) to expose a local port:

```bash theme={null}
ngrok http 3000
# Forwarding: https://abc123.ngrok.io -> http://localhost:3000
```

Register that URL as a webhook:

```bash theme={null}
curl -X POST https://edpire.com/api/v1/webhooks \
  -H "Authorization: Bearer edp_live_<your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/edpire",
    "events": ["submission.graded"]
  }'
```

Store the `secret` from the response. Complete an assessment — the webhook fires to your local handler in real time.

## Verify signature verification

Confirm your signature verification logic rejects tampered requests:

```typescript theme={null}
// Send a request with a wrong signature — your handler should return 401
const fakeSignature = "sha256=0000000000000000000000000000000000000000000000000000000000000000"
const res = await fetch("http://localhost:3000/webhooks/edpire", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Edpire-Signature": fakeSignature,
  },
  body: JSON.stringify({ event: "submission.graded", submission_id: "test" }),
})
assert(res.status === 401)
```

## Test idempotency

The same webhook event can be delivered more than once. Your handler must be safe to call twice with the same payload:

1. Complete an assessment — your handler processes the `submission.graded` event
2. Replay the exact same POST body to your webhook handler (copy from ngrok's request log)
3. Verify your database has exactly **one** record, not two

```typescript theme={null}
// Good — idempotent check before write
if (await db.submissionExists(req.body.submission_id)) {
  return res.sendStatus(200) // already processed
}
await db.saveSubmission(...)
```

## Test error paths

Don't only test the happy path. Verify your integration handles:

| Scenario              | How to trigger                                                                                |
| --------------------- | --------------------------------------------------------------------------------------------- |
| Invalid API key       | Use a wrong or revoked key — expect `401`                                                     |
| Missing scope         | Use a key without `read:results` on a submissions call — expect `403`                         |
| Assessment not found  | Request a non-existent assessment ID — expect `404`                                           |
| Max attempts exceeded | Submit the same assessment more than its allowed attempt count — expect `409`                 |
| Rate limit (general)  | Send 301+ requests per minute on any endpoint — expect `429` with `Retry-After`               |
| Rate limit (submit)   | Send 101+ requests per minute to `/assessments/{id}/submit` — expect `429`                    |
| Check lockout         | Call `/check` a 4th time for the same question and `session_id` within an hour — expect `429` |

## Pre-production checklist

There is no separate test key type: you develop and go live with the same `edp_live_` key (see [Keeping test data out of your real numbers](#keeping-test-data-out-of-your-real-numbers) below). This is the list to work through before you point real learners at the integration:

* [ ] All API calls go through your server — no `edp_live_` keys in the browser
* [ ] `return_url` and `back_url` match **Allowed API & Redirect Origins**; embedding pages match **Allowed Embed Origins**
* [ ] Webhook signature verification is enabled and tested
* [ ] Idempotency check is in place for `submission.graded`
* [ ] Error responses are handled (retry on 429/500, alert on persistent 5xx)
* [ ] `learner_ref` is your stable internal user ID — not email, not username
* [ ] `EDPIRE_API_KEY` is in environment variables, not committed to source control

See the full [Security checklist](/developer/security) before going live.
