Getting Started
Creating an application
After signing up here and go to the developers link at the dashboard
Fill the name and the redirectURI for your application
After created write down the clientId and ClientSecret of your application.
Authorization Code flow
When you create an application, you will receive a client_id for Authorization Code Flow, you will also need to specify a Redirect Uri. This is the url that the user will be redirected to after the flow.
By sending your user through the Invoce Processor Authorization flow, you can get permission to access their data. They will signup or login to an existing accoun.
sequenceDiagram
participant User
participant PartnerApp
participant InvprocAPI
participant InvprocPartnerLogin
participant PartnerCallBack
participant PartnerRedictPage
participant InvProcAPI
User->>PartnerApp: Login
PartnerApp->>InvprocAPI: POST/auth credentials state code_challenge
InvprocAPI->>InvprocAPI: validate credentials
InvprocAPI->>InvprocAPI: encrypt login URL (state and code_challlenge)
InvprocAPI->>PartnerApp: login URL 307
PartnerApp->>User: login URL 307
User->>InvprocPartnerLogin: login
InvprocPartnerLogin->>InvprocPartnerLogin: handle Login
InvprocPartnerLogin->>PartnerApp: authorization code + state
PartnerApp->>InvprocAPI: GET/token with authorization_code code_verifier
InvprocAPI->>PartnerApp: access and refresh token
PartnerApp->>InvProcAPI: do API calls on behalf of the user
The Login URL call (the auth call)
Initiate the Authorization code flow. To be able to initiate the Authorization flow, first the application needs a state and code_verifier which is a randomly generate, high entropy string between 43 and 128 characters. Store it, you'll need it later to fetch the access_token and to verify againt CSRF.
A successful response will be a temporary redirect to the Invoice Processor Login screen.
Please refer to the API docs for futher details
Below a fully functional example with react server action
- Run NextJs boilder app
- in the app folder create two folders one for login and one for callback, you are free to use any name as long as the callback one matches the redirectURI (i.e. if you named the folder callback the redirect URI will be http://localhost:3000/callback)
- Copy both files action.ts and page.tsx to the login folder and both files at the callback section to the callback folder
'use server';
import crypto from 'crypto';
import { cookies } from 'next/headers';
// Helper functions
function generateCodeVerifier() {
return crypto.randomBytes(50).toString('hex');
}
function generateCodeChallenge(codeVerifier: string) {
return crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
function generateState(length: number = 32): string {
return crypto.randomBytes(length)
.toString("base64url")
.slice(0, length);
}
export async function initiateAuthFlow() {
const clientId = 'YOUR CLIENT ID HERE';
const clientSecret = 'YOUR CLIENT SECRET HERE';
// Generate and store the code verifier and state
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
const state = generateState(32);
// Store these values in cookies so they can be retrieved later
// THIS IS FOR DEMONSTRATION PURPOSES ONLY DO STORE AUTHENTICATION INFO ON THE CLIENT SIDE, USE SERVER SIDE SESSIONS IN PRODUCTION
const cookieStore = cookies();
(await cookieStore).set('codeVerifier', codeVerifier, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 10, // 10 minutes
path: '/'
});
(await cookieStore).set('state', state, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 10, // 10 minutes
path: '/'
});
try {
const response = await fetch('https://api.invproc.com/api/oauth2/auth', {
method: 'POST',
redirect: 'manual', // Prevent automatic following of redirects
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
code_challenge: codeChallenge,
state: state,
isSignup: false
})
});
if (response.status === 302 || response.status === 307) {
const location = response.headers.get('Location');
if (location) {
return { success: true, redirectUrl: location };
}
}
return {
success: false,
error: `Unexpected response status: ${response.status}`
};
} catch (error) {
console.error('Auth flow initiation error:', error);
return {
success: false,
error: 'Failed to initiate authentication flow'
};
}
}
'use client';
import { useEffect, useState } from 'react';
import { initiateAuthFlow } from './actions';
export default function PartnerTest() {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const startAuth = async () => {
try {
const result = await initiateAuthFlow();
if (result.success && result.redirectUrl) {
// Redirect the user to the authentication URL
window.location.href = result.redirectUrl;
} else {
setError(result.error || 'Unknown error occurred');
setIsLoading(false);
}
} catch (err) {
console.error('Error starting auth flow:', err);
setError('Failed to start authentication process');
setIsLoading(false);
}
};
startAuth();
}, []);
if (isLoading) {
return <div>Initializing authentication...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
return <div>Partner Test</div>;
}
The Callback
After the User sucessfully completes the Login the configured redirect URI is going to be called with two url parameters code and state.
the code parameter
This it the authorization code that should be use to exchange for the authorization tokens (access and refresh tokens)
verify the state parameter
Please make sure to verify the received state parameter against the one you stored previously
//get the tokens from the server action
'use server';
import { cookies } from "next/headers";
export async function getTokens(code: string, state: string) {
// read state and codeVerifier from cookies
const cookieStore = await cookies();
const codeVerifier = cookieStore.get('codeVerifier');
const clientId = 'YOUR CLIENT ID HERE';
const clientSecret = 'YOUR CLIENT SECRET HERE';
const storedState = cookieStore.get('state');
if (!codeVerifier || !storedState) {
throw new Error('No codeVerifier or state found');
}
// VERIFY STATE TO CHECK AGAINST CSRF ATTACKS
if (storedState.value !== state) {
throw new Error('Invalid state');
}
//const { client_id, client_secret, authorization_code, code_verifier } = req.body;
const response = await fetch(`https://api.invproc.com/api/oauth2/tokens`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, authorization_code: code, code_verifier: codeVerifier.value }),
});
if (!response.ok) {
throw new Error('Failed to fetch tokens');
}else{
const data = await response.json();
return data;
}
}
'use client';
import { useSearchParams } from "next/navigation";
import { getTokens } from "./actions";
import { Suspense, useEffect, useState } from "react";
function RenderResult(){
const searchParams = useSearchParams();
const code = searchParams.get('code')||'';
const state = searchParams.get('state')||'';
const [accessToken, setAccessToken] = useState<string | null>(null);
const [refreshToken, setRefreshToken] = useState<string | null>(null);
useEffect(() => {
getTokens(code, state).then((tokens) => {
setAccessToken(tokens.accessToken);
setRefreshToken(tokens.refreshToken);
});
}, [code, state]);
return <div>Access Token: {accessToken} Refresh Token: {refreshToken}</div>;
}
export default function PartnerTestCallback() {
return (
<Suspense fallback={<div>Loading...</div>}>
<RenderResult />
</Suspense>
)
}
The Token exchange
Call the token exchange end point with your client credentials, the received authorization code and the previously stored code verifier