The model
A consent is a row in our database that says:User U has granted Partner L access to scope S until expiry E.Every snapshot fetch must reference an active consent. The user can revoke at any time. The scope determines what the snapshot includes. The expiry caps how long the access lasts. Crucially, the consent is held on Guild’s side, not the partner’s. You don’t store a copy of the user’s grant. You reference the consent by its ID on every request. We check it’s still active before we serve the snapshot.
How a user grants consent
Two paths.Path 1: User-initiated (from their Guild dashboard)
The user openspartners.guild.com.ng. They see a “Connect a partner” button. They search for your organisation (we maintain a directory of approved partners). They tap Grant access.
A prompt explains exactly what you’ll be able to read:
X Financier is requesting access to your Guild data. They will be able to read:User taps Grant. We write a consent row. We fireAccess will expire on 14 November 2026 (90 days). You can revoke any time. [Grant] [Decline]
- Your standing tier and score
- Your audit ledger summary for the last 90 days
- Your KYC verification status (Tier 3)
consent.granted webhook to your endpoint. The webhook payload contains the consent ID, the user ID, the scopes, and the expiry.
Path 2: Partner-initiated (consent link in your product)
The more common pattern. Inside your loan application flow, you generate a consent link via the API:consentUrl. They sign into Guild with their phone + OTP if not already signed in. They see the consent prompt (same content as path 1). They grant or decline. We redirect them back to your callbackUrl with ?status=granted&consentId=cnst_... or ?status=declined.
This is the OAuth-shaped pattern. Familiar to most partner backends.
The scope vocabulary
Each consent specifies which scopes the partner is granted. Scopes compose; the snapshot you receive is the intersection of what you requested and what the user granted.
Two rules:
- Only one
ledger_window_*scope per consent. They’re mutually exclusive. repayment_historyanddisbursement_historyare aggregated across all partners; you don’t see which partner extended the credit, only that someone did.
The consent record
What you can read fromGET /partner/v1/consents/:consentId:
PENDING (consent link generated, not yet acted on), ACTIVE (user granted), EXPIRED (passed expiresAt), REVOKED (user pulled it).
Using a consent
Every snapshot fetch includes the consent ID:- The consent exists.
- It belongs to the partner associated with your API key.
- It hasn’t expired.
- It hasn’t been revoked.
- It includes the scopes needed for the data being returned.
error_code. Your code should treat each failure mode distinctly — consent_revoked means ask the user to re-grant, consent_expired means renew, consent_not_found means a programming bug.
Revocation
The user can revoke at any time frompartners.guild.com.ng. The moment they tap revoke:
- Consent status flips to
REVOKED.revokedAtis set. - Any in-flight snapshot fetches with that consent ID fail with
consent_revoked(no race condition — we check at the start of every request). consent.revokedwebhook fires to your endpoint.- The user sees a confirmation in their dashboard.
Renewal
When a consent approaches expiry (we configure a 14-day warning window by default), the user gets a notification in their Guild dashboard with a one-tap renewal:Your consent with X Financier expires in 7 days. Renew for another 90 days?If they tap Renew, the same scopes extend by another window. If they tap Decline, the consent expires on schedule. We fire
consent.renewed (or, on expiry, consent.expired) webhooks.
You can also drive renewal from your side — generate a new consent link with the same scopes and email it to the user a week before expiry.
Multi-product consent
A user might grant a single partner consent for multiple products (a savings sweep + a credit line + an inventory advance). The model handles this two ways:- One broad consent per partner. Grant
standing + ledger_window_90 + kyc_status + repayment_history, valid for 365 days. The partner uses this consent for all products. Simpler for the user. - Separate consents per product. Each product has its own consent and its own scope set. Cleaner audit trail but requires the user to tap grant multiple times.
Why not OAuth?
You’ll notice the model looks a lot like OAuth, but we don’t expose the/oauth/authorize redirect dance directly. Two reasons:
- The user must always come back to Guild for the prompt. OAuth assumes the redirect can be fully customised by the relying party; we prefer a consistent prompt that the user recognises across every partner. Consent links are the abstraction over this.
- No tokens. OAuth issues access tokens that the partner stores. We don’t — every snapshot fetch references a consent by ID, and we check on every call. This is more chatty but means a revocation propagates instantly across your infrastructure.
The audit trail on consent
Every consent state change writes to aconsent_audit table that’s append-only with BEFORE UPDATE OR DELETE triggers. The full history of grants, expiries, revocations, and renewals is permanent.
Both you and the user can read this audit. The user from their dashboard. The partner from GET /partner/v1/consents/:consentId/audit. Disputes resolve quickly because the trail is uneditable.
Next
Snapshots
The actual data you read once you have a consent.
Webhooks
Push notifications when consents grant, revoke, or expire.