# OOLP Contracts — Developer Guide

This is the narrative companion to the OpenAPI spec at `/openapi.json`. Together they're everything you need to integrate a partner platform with **Obi Okonkwo Legal Practitioners (OOLP)** and start issuing NBA-sealed contracts.

---

## 1. Concepts in 60 seconds

| Term | Meaning |
| --- | --- |
| **Partner** | Your platform (e.g. YAARD). You get an API key + HMAC secret. |
| **Template** | A contract template (Residential Tenancy, Commercial Lease, Quit Notice…). Templates declare which partner-data fields they need. |
| **Request** | One contract you ask OOLP to produce. Has a status, a fee, an intake URL, and (eventually) a sealed PDF. |
| **Brief** | OOLP's internal word for a request, grouped under the principal client (usually the landlord). |
| **Client** | The principal party — derived by OOLP automatically from `partner_data` based on the template's `principal_role`. |
| **Webhook URL** | An HTTPS endpoint **on YOUR servers**. You paste it into OOLP's Partner Settings. We POST signed events to it. |

> **You do not host an OOLP-side URL.** OOLP issues a tokenised `intake_url` for each request and forwards events to your webhook. You never receive PII or contract content — only metadata.

---

## 2. Five-minute quickstart

1. An OOLP admin creates your Partner record. You receive an email with:
   - `api_key` (starts with `oolp_`, shown once)
   - `hmac_secret` (shown once)
   - A magic-link login to your Partner dashboard
2. In Partner Settings on the OOLP dashboard, paste your `webhook_url`.
3. From your server, sign a request and POST it:

```js
// Node.js
import { createHmac } from 'crypto';

const body = JSON.stringify({
  template_slug: 'residential-tenancy',
  partner_reference: 'YAARD-LEASE-2026-00042',
  contract_value_ngn: 2_400_000,
  requester: { name: 'Adaeze Ibe', email: 'adaeze@yaard.ng' },
  partner_data: {
    landlord_name: 'Chidinma Okeke',
    landlord_email: 'chidinma@example.com',
    tenant_name:   'Tunde Bakare',
    tenant_email:  'tunde@example.com',
    property_address: '12 Akin Adesola, Victoria Island, Lagos',
    rent_ngn: 2_400_000,
    start_date: '2026-08-01',
    end_date:   '2027-07-31',
  },
});
const ts  = Math.floor(Date.now() / 1000).toString();
const sig = createHmac('sha256', HMAC_SECRET).update(`${ts}.${body}`).digest('hex');

const res = await fetch('${OOLP_BASE_URL}/api/public/v1/requests', {
  method: 'POST',
  headers: {
    'content-type':      'application/json',
    'x-oolp-api-key':    API_KEY,
    'x-oolp-timestamp':  ts,
    'x-oolp-signature':  sig,
  },
  body,
});
const { id, intake_url, fee_ngn } = await res.json();
```

4. Show `intake_url` to the client (redirect, modal, email — your call).
5. Listen for webhooks (next section). The final `contract.completed` event carries a `verify_url` to the sealed PDF.

---

## 3. Webhooks

### Who creates the URL?

**You do.** It is an HTTPS endpoint on your server. Paste it into Partner Settings on the OOLP dashboard.

### Receiving

```js
// Node.js / Express
import { createHmac, timingSafeEqual } from 'crypto';
import express from 'express';

const app = express();
app.post('/oolp/webhook', express.raw({ type: '*/*' }), (req, res) => {
  const ts  = req.header('x-oolp-timestamp');
  const sig = req.header('x-oolp-signature');
  const raw = req.body.toString('utf8');

  // Reject stale requests
  if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);

  const expected = createHmac('sha256', HMAC_SECRET).update(`${ts}.${raw}`).digest('hex');
  if (!sig || !timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return res.sendStatus(401);

  const event = JSON.parse(raw);
  // Idempotency: remember event.event_id for 24h
  enqueueForProcessing(event);
  res.sendStatus(200); // ack within 10s
});
```

### Events

| Event | When | Notable payload |
| --- | --- | --- |
| `request.created` | After your POST succeeds | `fee_ngn`, `intake_url` |
| `payment.required` | Awaiting payment | — |
| `payment.confirmed` | We received payment confirmation | `payment_reference` |
| `intake.submitted` | Client finished intake | — |
| `contract.sealed` | Lawyer applied NBA + firm seal | `file_number`, `sealed_at` |
| `contract.sent` | Signing links emailed | — |
| `contract.signed` | All parties signed | — |
| `contract.completed` | Final PDF ready | `verify_url` |

### Retries

Exponential backoff (1m, 5m, 30m, 2h, 12h). Up to 5 attempts. Return any 2xx within 10s to acknowledge.

### Replay protection

Cache `x-oolp-event-id` for 24h and ignore duplicates.

---

## 4. The widget

For sites that don't want to integrate server-side, embed the widget:

```html
<script
  src="${OOLP_BASE_URL}/api/public/v1/widget.js"
  data-partner-key="oolp_publishable_…"
  data-template="residential-tenancy"
  data-reference="YAARD-LEASE-2026-00042">
</script>
```

It opens the intake UI in a modal and emits `oolp:request_created`, `oolp:payment_confirmed`, `oolp:completed` events on `window` for your JS to listen to. Only the publishable key (not the HMAC secret) is needed.

---

## 5. Money, dates, and partner data

- All money is integer **NGN** (no kobo). Field names ending in `_ngn` are always integers.
- All dates are ISO 8601 (`YYYY-MM-DD` or full RFC 3339).
- `partner_data` keys must match the template's `required_field_keys`. Discover them at `GET /v1/templates/{slug}`.
- Anything extra is stored verbatim and visible to lawyers but never inserted into the contract body unless a clause explicitly references it.

---

## 6. Errors

All errors use `application/problem+json`:

```json
{
  "status": 422,
  "title":  "Validation failed",
  "code":   "validation_failed",
  "detail": "Field `partner_data.rent_ngn` is required for template `residential-tenancy`.",
  "errors": [{ "path": "partner_data.rent_ngn", "message": "Required." }]
}
```

| Status | Meaning |
| --- | --- |
| 400 | Malformed body |
| 401 | Bad/missing key, signature, or timestamp out of window |
| 404 | Template or request not found |
| 409 | Idempotency conflict — same key, different body |
| 422 | Validation failed (see `errors[]`) |
| 429 | Rate limited |

---

## 7. FAQ

**Can we re-send a webhook?**  Yes — in the OOLP dashboard, open the request and click "Resend" on any delivery.

**What if the client never finishes intake?**  The request sits in `intake` indefinitely. You can cancel by `POST /v1/requests/{id}/cancel` (rolls back payment if applicable).

**Can we update `partner_data` after creation?**  Until the contract is sealed, yes: `PATCH /v1/requests/{id}` with the same HMAC scheme. After sealing, contact OOLP.

**Where can we see contract content?**  You can't — partners are explicitly never given party PII or the contract body. Only the principal party, OOLP staff, and the assigned lawyer can. You receive a verification URL after completion so you can prove a sealed PDF exists.

**Sandbox/staging?**  Use whichever staging domain you deploy for OOLP. The API paths stay the same; only `OOLP_BASE_URL` changes.

---

For anything not covered here, email **engineering@obiokonkwo.com**.
