Developer guide
How to bid on VisionForgeX inventory over OpenRTB or Prebid, and how publishers pull their numbers out with the Reporting API. Everything here is what the platform does today.
POST https://bid.partner.example/openrtb
{ "id": "5f0c2a9e…e0f4",
"imp": [{ "banner": { "w": 300, "h": 250 }, "bidfloor": 0.25 }],
"site": { "domain": "example.com" },
"at": 1, "tmax": 800 }
200 OK
{ "seatbid": [{ "bid": [{
"price": 2.40, "w": 300, "h": 250,
"adm": "<a href=…><img …></a>",
"burl": "https://…?p=${AUCTION_PRICE}" }] }] }
Three ways to connect
Demand partners bid in one of two ways. Publishers who want their figures in their own systems use the third.
| Integration | Who it is for | Where it runs | What we need from you |
|---|---|---|---|
| OpenRTB 2.5 | Demand partners with their own bidder | Server to server: we call you | An HTTPS endpoint, and a header to authenticate with if you want one |
| Prebid.js | Demand partners with a Prebid adapter | In the visitor's browser | Your bidder code and one set of parameters |
| Reporting API | Publishers on VisionForgeX | Server to server: you call us | Nothing. We switch it on and you make a key |
Both bidding routes meet in the same auction, next to Google Ad Exchange. The highest price is the one shown, whoever it comes from.
What we serve
Partner bidding is open on display placements. The other formats on our publishers' pages run through Google Ad Manager's own formats and are not open to partner bids today.
| Format | OpenRTB | Prebid.js | Notes |
|---|---|---|---|
| Display (HTML) | Yes | Yes | Fixed-size placements. A request offers up to 12 sizes; bid on any one of them. |
| Native | No | Ask first | Title and image assets on a small number of sites. Agree it with us before you build. |
| Sticky, interstitial, rewarded | No | No | Served through Google Ad Manager. |
| Video | No | No | No in-stream or outstream. A VAST response is not rendered. |
The sizes you will see most are 300×250, 336×280, 728×90, 970×250, 300×600, 320×100 and 320×50. Each request lists exactly what that placement takes, and that list is the only authority.
- Web only. Desktop and mobile browsers. No in-app inventory.
- US dollars only. Floors, bids and settlement are in USD. A bid in another currency is dropped.
- Refresh. A placement that stays in view can refresh. Every refresh is a new auction with a new request.
- Reviewed sites. A site serves nothing until our team has approved it.
OpenRTB
We speak OpenRTB 2.5 over HTTPS, as JSON, server to server. Our server sends you a bid request for one display placement; you answer with a bid or with nothing. Nothing runs in the browser until you have won.
Before you start
Send us these at [email protected]:
- Your bid endpoint. One
https://address. Put any account or zone identifier in the address itself. - Authentication, if you use it. One header name and its value, such as
X-Api-KeyorAuthorization. We send it on every request. - Your floor. The lowest CPM in USD worth sending you. We put it in
bidfloorand drop bids under it. - Your time limit. Between 150 and 2,000 ms. Without one we use 800 ms.
- A technical contact who can read logs with us on the first day.
How the auction runs
- A placement is about to loadThe tag on the publisher's page asks our server for bids on it.
- Every partner is asked at onceOne request each, sent in parallel, each with its own
tmax. - Answers are collectedA late answer, an error or an empty one is a no-bid. Nothing is retried.
- The best price goes forwardThe highest valid bid among partners competes with Google Ad Exchange and the Prebid bidders.
- The winner is shown and toldIf it is yours, we render your markup and call your notice addresses.
It is a first-price auction (at is 1): if you win, you pay what you bid. To compete inside the ad server a price is rounded down to a price point: every cent up to $1, every 5 cents up to $3, every 25 cents up to $10 and every dollar up to $20. A bid above $20 competes as $20. The price we report back to you is always your own bid, unrounded.
The bid request
A POST to your endpoint with a JSON body. One request carries one impression.
POST /openrtb HTTP/1.1
Host: bid.partner.example
Content-Type: application/json
x-openrtb-version: 2.5
X-Api-Key: the-value-you-gave-us
{
"id": "5f0c2a9e41b7d3c86a12e0f4",
"imp": [
{
"id": "1",
"tagid": "vfx-article-top",
"banner": {
"w": 300,
"h": 250,
"format": [
{ "w": 300, "h": 250 },
{ "w": 336, "h": 280 }
]
},
"secure": 1,
"bidfloorcur": "USD",
"bidfloor": 0.25
}
],
"site": {
"id": "example-news",
"domain": "example.com",
"page": "https://example.com/world/a-story-worth-reading"
},
"device": {
"ua": "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Mobile Safari/537.36",
"ip": "203.0.113.24",
"language": "en"
},
"cur": ["USD"],
"at": 1,
"tmax": 800
}
| Field | Type | What we send |
|---|---|---|
id | string | The auction ID, 24 hexadecimal characters. New for every request, including every refresh. |
imp[0].id | string | Always "1". There is one impression per request. |
imp[0].tagid | string | The placement's ID on the page, up to 64 characters. Stable for a placement, so you can learn from it. |
imp[0].banner.format | array | Every size the placement takes, up to 12. banner.w and banner.h repeat the first one. |
imp[0].secure | integer | Always 1. Every asset in your creative has to load over HTTPS. |
imp[0].bidfloor | number | Your floor, as a CPM. 0 when you have not set one. bidfloorcur is always USD. |
site.id | string | Our identifier for the site. It does not change. |
site.domain | string | The site's domain, without www. |
site.page | string | The address of the page the placement is on. The site's home page when the real one is not available. |
device.ua | string | The visitor's browser user agent, up to 512 characters. |
device.ip | string | The visitor's IP address, IPv4 or IPv6. Use it for geography and for your own traffic checks. |
device.language | string | Two-letter browser language, when the browser sends one. |
cur | array | Always ["USD"]. |
at | integer | 1, first price. |
tmax | integer | Milliseconds we wait for you, counted from the moment we send. Your limit, or less when the page's own budget is shorter. |
What is not in the request. There is no user object, no buyer ID and no cookie matching, and no regs or source object. Bid on the page, the placement, the device and the location you derive from the IP address. Do not rely on a field that is not in the table above.
Your bid response
Answer 200 with a standard bid response to bid. To pass, answer 204 with no body, which is the cheapest thing for both of us.
{
"id": "5f0c2a9e41b7d3c86a12e0f4",
"bidid": "b-7741",
"cur": "USD",
"seatbid": [
{
"seat": "seat-42",
"bid": [
{
"id": "1",
"impid": "1",
"price": 2.4,
"w": 300,
"h": 250,
"adid": "cr-889",
"crid": "889",
"adomain": ["advertiser.example"],
"adm": "<a href=\"https://click.partner.example/c?b=${AUCTION_BID_ID}\" target=\"_blank\" rel=\"noopener\"><img src=\"https://cdn.partner.example/889.jpg\" width=\"300\" height=\"250\" alt=\"\"></a>",
"nurl": "https://win.partner.example/w?p=${AUCTION_PRICE}&id=${AUCTION_ID}",
"burl": "https://bill.partner.example/b?p=${AUCTION_PRICE}&imp=${AUCTION_IMP_ID}"
}
]
}
]
}
A bid is accepted when all of this is true:
- The status is
200, the body is JSON with aseatbidarray, and it arrived withintmax. priceis a number above zero, at or above your floor, and no more than $200 CPM.admis a non-empty string of HTML.wandhare one of the sizes in the request. Leave them out and we take the first size.curisUSDor left out.
| Field | Needed | How we use it |
|---|---|---|
seatbid[].bid[].price | Yes | Your bid as a CPM in USD. If you send several bids, the highest valid one is used. |
seatbid[].bid[].adm | Yes | The creative, as HTML. Macros in it are filled in before it is shown. |
seatbid[].bid[].w, h | Recommended | The creative's size. It decides the size of the frame. |
seatbid[].bid[].nurl | Optional | Win notice. Called when the creative is shown. |
seatbid[].bid[].burl | Optional | Billing notice. Called when the creative is shown. |
seatbid[].bid[].impid | Optional | Echoed in ${AUCTION_IMP_ID}. "1" when left out. |
seatbid[].bid[].adid | Optional | Echoed in ${AUCTION_AD_ID}. |
seatbid[].seat | Optional | Echoed in ${AUCTION_SEAT_ID}. |
bidid | Optional | Echoed in ${AUCTION_BID_ID}. |
adomain, crid, cat | Recommended | Not used by the auction. Send them: they are what we ask for when a publisher reports a creative. |
We do not send loss notices. If you hear nothing after bidding, you did not win or the creative was never shown.
Macros and notices
These are replaced in adm, nurl and burl:
| Macro | Becomes |
|---|---|
${AUCTION_PRICE} | Your bid price in USD, as a plain number: 2.4. Not encrypted, since you pay your own bid. |
${AUCTION_ID} | The request's id. |
${AUCTION_BID_ID} | Your response's bidid. |
${AUCTION_IMP_ID} | Your bid's impid. |
${AUCTION_SEAT_ID} | Your seat. |
${AUCTION_AD_ID} | Your bid's adid. |
${AUCTION_CURRENCY} | USD. |
${AUCTION_MBR}, ${AUCTION_LOSS} | Nothing. They are removed. |
When the notices are called. Not when you win the auction, but when your creative is placed on the page. At that moment our server calls nurl and burl with a GET, once each. Most winning bids are shown within a second or two.
- Bill on the notice, never on the bid response. A bid that was not shown is not notified and is not owed.
- Each address must start with
http://orhttps://and be no longer than 2,000 characters after the macros are filled in. - We wait 1.5 seconds for your answer and do not retry. Answer fast and with any
2xx. - A creative shown more than 30 minutes after the bid is not notified. This is rare; it is a tab left open in the background.
- The calls come from our server, not from the visitor's browser, so they carry no visitor cookies. For a browser-side count, put your own pixel in
adm.
Creative rules
Your markup is written into a sandboxed frame of exactly the size you bid. The frame allows scripts and allows a click to open a new tab. It does not give access to the publisher's page, its cookies or its storage.
<!-- Everything over https. The click opens a new tab. -->
<a href="https://click.partner.example/c?b=${AUCTION_BID_ID}" target="_blank" rel="noopener">
<img src="https://cdn.partner.example/889.jpg" width="300" height="250" alt="">
</a>
<img src="https://px.partner.example/i?p=${AUCTION_PRICE}" width="1" height="1" alt="" style="position:absolute">
- HTML only. No VAST, no native JSON, no MRAID.
- HTTPS only. One asset over plain HTTP and the browser blocks it.
- Open clicks in a new tab with
target="_blank". Navigating the publisher's page from inside the frame is blocked. - Stay inside the frame. No expanding, no covering the page, no resizing.
- Nothing the visitor did not ask for. No redirects, no pop-ups or downloads without a click, no sound until a click.
- No reading of the page. The frame has its own empty origin, so cookies and local storage from your own domain are not available inside it either.
A creative that breaks these rules gets its partner switched off until it is fixed. Publishers can report a creative to us, and we will come to you with the page, the time and the price.
Backfill requests
Some sites ask partners only after Google Ad Exchange has returned nothing for a placement. Those requests come to the same endpoint and differ in a few ways. If any of them is a problem, say so and we keep you on the standard requests only.
| Standard | Backfill | |
|---|---|---|
| When you are asked | At the start, alongside Google | After Google had no ad |
at | 1 | 2. You still pay your own bid. |
Request id | 24 characters | 16 characters |
| Fields left out | None | site.id, device.language, cur, bidfloorcur and the x-openrtb-version header |
| Bids read | Every bid in every seat | The first bid of the first seat |
| Notices | nurl and burl, when shown | nurl only, when your bid is chosen |
| Macros | All of them, in adm and both notices | ${AUCTION_PRICE} in nurl only |
Test and go live
- Send us your details. The list under Before you start.
- We add your endpoint. From that moment you receive real bid requests. Answer
204until you are ready to bid. - Check what arrives. Confirm that the body parses, the sizes are ones you buy and the floor is the one you gave us.
- Bid on a page we agree. We watch one of your creatives render and confirm both notices reached you with the right price.
- Compare the first full day. Your billed impressions against our count of wins. Tell us if they are more than a few percent apart, before volume grows.
If you allow traffic by IP address, ask us for the addresses our requests come from.
Prebid.js
We run our own build of Prebid.js 11.29 on every site that has Prebid switched on. Adapters are compiled into that build, so a new bidder means a new build from us. There is nothing for the publisher to install.
Our build
- Consent. The TCF and GPP consent modules are included, with the GPP sections for US national and US state laws. Where the publisher's page has a consent tool, your adapter receives the consent strings the usual Prebid way.
- GPID. The pre-auction module passes the ad server's slot path to your adapter.
- User sync. Prebid's defaults: image pixels after the auction, no iframe syncs.
- Storage. Storage access goes through Prebid's storage control, so an adapter that writes a cookie has to declare it the way Prebid requires.
What we need from you
- Your adapter's name in the Prebid.js repository. It has to be an official adapter that works on Prebid 11.
- Your bidder code, as Prebid knows it.
- One set of parameters. See the note below.
- What you buy: display sizes, and native if we have agreed it.
- Your ads.txt lines for our publishers. See ads.txt and sellers.json.
One set of parameters covers everything. The same params object is sent for every placement on every site. Give us account-level parameters, such as a network or publisher ID. An integration that needs a different placement ID for each slot will not work as it stands; tell us and we will find the right setup with you.
How your adapter is called
// What we register for each placement. You do not write this; it is here so you know what your adapter receives.
pbjs.addAdUnits([{
code: "vfx-article-top", // the placement's element id
mediaTypes: { banner: { sizes: [[300, 250], [336, 280]] } },
bids: [
{ bidder: "yourBidder", params: { networkId: "12345" } } // the one set of parameters you give us
]
}]);
pbjs.setConfig({ bidderTimeout: 1200 });
- Timeout.
bidderTimeoutis 1,200 ms unless we have agreed another value. A bid that arrives later is ignored. - Currency. Bid in USD. Prebid's currency module is not in the build, so nothing is converted, and on most sites a bid in another currency is dropped.
- Price. Your bid is rounded down to a price point to compete in the ad server, the same ladder as for OpenRTB on most sites. Prebid reports your own bid price back to you.
- Winning. Prebid tells your adapter in the usual way (
onBidWonand your creative's own pixels). We add nothing of our own to it. - Creative. Rendered in a sandboxed frame. The creative rules are the same.
- Refresh. A refreshed placement runs a new Prebid auction.
Test and go live
- Send us your details. The list under What we need from you.
- We add your adapter to the build and your bidder to our list, switched off.
- We switch you on. Your adapter starts receiving bid requests from every site that runs Prebid.
- Check a live page together. Add
?pbjs_debug=trueto a page's address and Prebid logs your requests and bids to the browser console. - Compare the first full day. Your paid impressions against the wins we count.
ads.txt and sellers.json
Send us the ads.txt lines your buyers expect to find. We add them to the ads.txt every one of our publishers is asked to carry, and the dashboard checks each site for them and chases the publisher when a line is missing.
Each site on VisionForgeX has its own numeric seller ID. A publisher's ads.txt carries a line in this form for us:
# The line a publisher carries for VisionForgeX. The number is that site's seller ID.
visionforgex.com, 48213907, DIRECT
Reporting API
For publishers. The API returns the figures on the Reports page of your dashboard: impressions, clicks, revenue, eCPM and CTR, by day, by domain and by ad format, as JSON or CSV. It only reads. Nothing on your account can be changed through it.
Access
The API is off until we switch it on for your account. Ask your account manager or write to [email protected]. Once it is on, a Reporting API page appears in your dashboard menu.
A key returns the sites on its own account and nothing else. That is decided on our server from the account the key belongs to; no option in a request can widen it.
Keys and your first call
Make your key on the Reporting API page of the dashboard. It is shown once, so copy it into your server's secrets when you make it. There is one key for an account; making a new one stops the old one at once.
Send the key in a header on every request:
export VFX_API_KEY="vfx_your_key_here"
curl "https://visionforgex.com/api/v1/" \
-H "Authorization: Bearer $VFX_API_KEY"
{
"data": {
"account": "Example Media",
"email": "[email protected]",
"access": "on",
"sites": 2,
"limits": {
"requests_per_minute": 60,
"requests_per_day": 5000,
"max_days_per_request": 90
},
"endpoints": {
"sites": "https://visionforgex.com/api/v1/sites/",
"reports": "https://visionforgex.com/api/v1/reports/"
}
},
"meta": {
"version": "v1",
"data_updated_at": "2026-10-06T21:40:12Z",
"docs": "https://visionforgex.com/developers/#reporting-api"
}
}
Authorization: Bearer <key>is the header to use.X-API-Key: <key>works too, for tools that cannot set the first one.- A key in the address (
?key=…) is refused. Addresses end up in logs. - Call from a server. A key in a web page or a mobile app can be read by anyone who opens it, and the API does not answer browser cross-origin requests.
- Every address is
GETonly, over HTTPS, and ends with a slash.
Reports
GEThttps://visionforgex.com/api/v1/reports/
curl "https://visionforgex.com/api/v1/reports/?from=2026-09-30&to=2026-10-06&group_by=date,domain" \
-H "Authorization: Bearer $VFX_API_KEY"
<?php
$url = 'https://visionforgex.com/api/v1/reports/?' . http_build_query([
'from' => '2026-09-30',
'to' => '2026-10-06',
'group_by' => 'date,domain',
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('VFX_API_KEY')],
CURLOPT_TIMEOUT => 20,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$report = json_decode((string) $body, true);
if ($status !== 200) {
throw new RuntimeException($report['error']['message'] ?? 'The request failed');
}
foreach ($report['data'] as $row) {
echo $row['date'], ' ', $row['domain'], ' ', $row['revenue'], "\n";
}
import os
import requests
response = requests.get(
"https://visionforgex.com/api/v1/reports/",
headers={"Authorization": f"Bearer {os.environ['VFX_API_KEY']}"},
params={"from": "2026-09-30", "to": "2026-10-06", "group_by": "date,domain"},
timeout=20,
)
report = response.json()
if response.status_code != 200:
raise RuntimeError(report["error"]["message"])
for row in report["data"]:
print(row["date"], row["domain"], row["revenue"])
// Node.js 18 or later
const params = new URLSearchParams({ from: "2026-09-30", to: "2026-10-06", group_by: "date,domain" });
const response = await fetch(`https://visionforgex.com/api/v1/reports/?${params}`, {
headers: { Authorization: `Bearer ${process.env.VFX_API_KEY}` },
});
const report = await response.json();
if (!response.ok) throw new Error(report.error.message);
for (const row of report.data) {
console.log(row.date, row.domain, row.revenue);
}
| Option | Value | What it does |
|---|---|---|
from, to | YYYY-MM-DD | The first and last day, both included. At most 90 days in one request. |
days | 1 to 90 | Instead of from and to: the last N days, today included. With neither, you get the last 7 days. |
group_by | date, domain, ad_format | One or more, separated by commas. One row per combination. total gives a single row for the whole period. Default: date. |
domain | example.com | Only these domains of your account, separated by commas. Leave it out for all of them. |
ad_format | banner, anchor, interstitial, rewarded, native | Only these formats, separated by commas. |
format | json or csv | How the answer is written. Default: json. |
{
"data": [
{
"date": "2026-09-30",
"domain": "example.com",
"impressions": 48210,
"clicks": 395,
"revenue": 86.7312,
"ecpm": 1.8,
"ctr": 0.82
},
{
"date": "2026-09-30",
"domain": "example.org",
"impressions": 9120,
"clicks": 61,
"revenue": 11.4004,
"ecpm": 1.25,
"ctr": 0.67
}
],
"totals": {
"impressions": 57330,
"clicks": 456,
"revenue": 98.1316,
"ecpm": 1.71,
"ctr": 0.8
},
"meta": {
"from": "2026-09-30",
"to": "2026-09-30",
"group_by": ["date", "domain"],
"filters": { "domain": [], "ad_format": [] },
"currency": "USD",
"rows": 2,
"complete": true,
"pending_conversion_rows": 0,
"data_updated_at": "2026-10-06T21:40:12Z"
}
}
| Field | Type | Meaning |
|---|---|---|
data[].date | string | The reporting day, YYYY-MM-DD. Present when you group by date. |
data[].domain | string | The site's domain, without www. Present when you group by domain. |
data[].ad_format | string | One of banner, anchor, interstitial, rewarded, native. Present when you group by ad_format. |
impressions | integer | Ads shown. |
clicks | integer | Clicks on them. |
revenue | number | Revenue in US dollars, to four decimal places. The same figure as your Reports page, before your revenue share is applied. |
ecpm | number | Revenue for each thousand impressions, in US dollars. |
ctr | number | Clicks as a percentage of impressions. 0.82 means 0.82%. |
totals | object | The same five figures for everything the request matched. |
meta.complete | boolean | false while some rows are still waiting to be converted to US dollars. Their impressions and clicks are counted; their revenue is added when the conversion is done. pending_conversion_rows says how many. |
meta.data_updated_at | string | When the figures were last refreshed, in UTC. |
Rows come back in order: by date, then domain, then format. A combination with no impressions has no row, so fill gaps with zero on your side.
With format=csv the same rows come back as a file, with the unit in each column name:
date,domain,impressions,clicks,revenue_usd,ecpm_usd,ctr_percent
2026-09-30,example.com,48210,395,86.7312,1.80,0.82
2026-09-30,example.org,9120,61,11.4004,1.25,0.67
Recent days move. The figures are refreshed several times a day. Today is always part of a day, and the last two or three days can still be corrected as Google finalises them. Fetch them again before you treat them as final.
Sites
GEThttps://visionforgex.com/api/v1/sites/
The domains your key can report on. Use it to check a domain's spelling before you filter by it.
{
"data": [
{ "domain": "example.com", "status": "live" },
{ "domain": "example.org", "status": "waiting_for_approval" }
],
"meta": { "count": 2 }
}
status is live for a site that is approved and serving, and waiting_for_approval for one our team has not reviewed yet.
Account
GEThttps://visionforgex.com/api/v1/
Which account the key belongs to, how many sites it covers, the limits, and when the figures were last refreshed. The answer is the one shown under Keys and your first call. It is the right call for a health check.
Errors
An error has an HTTP status and a body in one shape. Branch on code; message is written for a person and can change. Quote request_id when you write to us. It is also in the X-Request-Id header of every answer.
{
"error": {
"code": "range_too_long",
"message": "One request covers at most 90 days. Ask for a longer period in several requests.",
"request_id": "9b1f04c2a7d35e68"
}
}
| Status | Code | What happened |
|---|---|---|
400 | invalid_period, invalid_date, range_too_long | The dates are missing, not real dates, in the wrong order, in the future, or more than 90 days apart. |
400 | invalid_group_by, invalid_ad_format, invalid_format | An option has a value it does not take. |
400 | unknown_domain | A domain in the filter is not on your account. |
401 | missing_key, key_in_address | No key in a header. |
401 | invalid_key | The key is wrong, or was replaced or revoked. |
403 | access_off | The API is switched off for the account. |
403 | account_closed | The account is closed. |
405 | method_not_allowed | Something other than GET. |
429 | rate_limited | Too many requests. Wait the number of seconds in the Retry-After header. |
429 | too_many_attempts | Many wrong keys from your address. Wait ten minutes. |
503 | reports_unavailable | Our side. Try again in a minute. |
Limits and good habits
- 60 requests a minute and 5,000 a day for an account. The day starts at 00:00 UTC.
- Every answer carries
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(a Unix time). - 90 days in one request. For a longer history, ask month by month.
- The figures change a few times a day, not every minute. Once an hour is as often as it is worth asking.
- Keep the key out of your code repository. Read it from an environment variable or a secrets store, as the examples do.
A daily job that keeps a copy of your figures up to date is one line:
# Once a day, a little after midnight UTC: yesterday is new, and the two days before it may have been corrected.
curl "https://visionforgex.com/api/v1/reports/?days=4&group_by=date,domain,ad_format&format=csv" \
-H "Authorization: Bearer $VFX_API_KEY" \
-o visionforgex-latest.csv
The address has v1 in it. We add fields and options to v1 without notice, so ignore fields you do not know. Anything that would break a working integration goes into a new version, and v1 keeps running beside it.
Talk to a person
Integrations are set up by our team, not by a form. Tell us what you are connecting and we will answer with the next step, usually the same day.